Merge pull request #743 from apache/feature/685-properties-json-serialization
Feature/685 properties json serialization
diff --git a/CHANGES.md b/CHANGES.md
index 8b30440..2720590 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -71,6 +71,8 @@
Celix event thread.
- Apache Celix filter now use the underlying properties value types for matching. This means that it is more important
to add service properties with the correct type.
+- Celix C++ Exception are now defined in the `celix/exceptions.h` header file. The `celix/Exception.h`
+ and `celix/IOException.h` are removed.
## New Features
diff --git a/conanfile.py b/conanfile.py
index dbb748a..4e42300 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -309,7 +309,7 @@
self.options['openssl'].shared = True
if self.options.build_celix_dfi:
self.options['libffi'].shared = True
- if self.options.build_celix_dfi or self.options.build_celix_etcdlib:
+ if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib:
self.options['jansson'].shared = True
def requirements(self):
@@ -332,7 +332,7 @@
self.requires("civetweb/1.16")
if self.options.build_celix_dfi:
self.requires("libffi/[>=3.2.1 <4.0.0]")
- if self.options.build_celix_dfi or self.options.build_celix_etcdlib:
+ if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib:
self.requires("jansson/[>=2.12 <3.0.0]")
if self.options.build_rsa_discovery_zeroconf:
# TODO: To be replaced with mdnsresponder/1790.80.10, resolve some problems of mdnsresponder
diff --git a/documents/README.md b/documents/README.md
index 3930498..c174acb 100644
--- a/documents/README.md
+++ b/documents/README.md
@@ -86,6 +86,7 @@
* [Apache Celix C Patterns](c_patterns.md)
* Utils
* [Apache Celix Properties & Filter](properties_and_filter.md)
+ * [Apache Celix Properties Encoding](properties_encoding.md)
* Framework
* [Apache Celix Bundles](bundles.md)
* [Apache Celix Services](services.md)
diff --git a/documents/properties_encoding.md b/documents/properties_encoding.md
new file mode 100644
index 0000000..c4cdb38
--- /dev/null
+++ b/documents/properties_encoding.md
@@ -0,0 +1,333 @@
+---
+title: Apache Celix Properties Encoding
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Apache Celix Properties JSON Encoding
+
+## Introduction
+
+In Apache Celix, properties represent key-value pairs, often used for configuration. While these properties are not JSON
+objects inherently, they can be encoded to and decoded from JSON for interoperability or storage. This page explains how
+Apache Celix properties are encoded to and decoded from JSON.
+
+### Encoding limitations
+
+Except for empty arrays and the double values NaN, Infinity, and -Infinity, all Apache Celix properties types can
+be encoded to JSON.
+
+The reason for the empty array limitation is that for a properties array entry the array list element type is must be
+known, this is not possible to infer from an empty JSON array. To ensure that everything this is encoded, can be decoded
+again, a properties array entry with an empty array is not encoded to JSON.
+
+The reason for the double values NaN, Infinity, and -Infinity limitation is that JSON does not support these values.
+
+### Decoding limitations
+
+When decoding JSON to Apache Celix properties, the following limitations apply:
+
+- Mixed array types are not supported. For example, an array with both strings and longs cannot be decoded to a
+ properties' entry.
+- null values are not supported, because properties does not support a null value type.
+- Empty arrays are not supported, because the array list element type must be known, this is not possible to infer from
+ an empty JSON array.
+- JSON keys that collide on the created properties' key level are not supported.
+ See [Properties Decoding](##Properties Decoding) for more information.
+
+## Properties Encoding
+
+Apache Celix properties can be encoded to JSON using the `celix_properties_save`, `celix_properties_saveToStream`
+and `celix_properties_saveToString` functions. These functions take a properties object and encode it to a JSON object
+string. The encoding can be controlled using flags and can be done in a flat or nested structure.
+
+### Properties Flat Encoding
+
+By default, the encoding is done in a flat structure, because a flat structure ensures that all keys of the properties
+can be represented in JSON format. When properties are encoded to JSON in a flat structure, the reverse operation,
+decoding JSON that has been encoded from properties, will result in the same properties (except for the previously
+mentioned limitations (empty arrays and the double values NaN, Infinity, and -Infinity)).
+
+Flat Encoding example:
+
+```C
+#include <stdio.h>
+#include <celix/properties.h>
+
+int main() {
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+ celix_properties_set(props, "single/strKey", "strValue");
+ celix_properties_setLong(props, "single/longKey", 42);
+ celix_properties_setDouble(props, "single/doubleKey", 2.0);
+ celix_properties_setBool(props, "single/boolKey", true);
+ celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier"));
+
+ celix_array_list_t* strArr = celix_arrayList_createStringArray();
+ celix_arrayList_addString(strArr, "value1");
+ celix_arrayList_addString(strArr, "value2");
+ celix_properties_assignArrayList(props, "array/stringArr", strArr);
+
+ celix_array_list_t* longArr = celix_arrayList_createLongArray();
+ celix_arrayList_addLong(longArr, 1);
+ celix_arrayList_addLong(longArr, 2);
+ celix_properties_assignArrayList(props, "array/longArr", longArr);
+
+ celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray();
+ celix_arrayList_addDouble(doubleArr, 1.0);
+ celix_arrayList_addDouble(doubleArr, 2.0);
+ celix_properties_assignArrayList(props, "array/doubleArr", doubleArr);
+
+ celix_array_list_t* boolArr = celix_arrayList_createBoolArray();
+ celix_arrayList_addBool(boolArr, true);
+ celix_arrayList_addBool(boolArr, false);
+ celix_properties_assignArrayList(props, "array/boolArr", boolArr);
+
+ celix_array_list_t* versionArr = celix_arrayList_createVersionArray();
+ celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier"));
+ celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier"));
+ celix_properties_assignArrayList(props, "array/versionArr", versionArr);
+
+ celix_properties_saveToStream(props, stdout, CELIX_PROPERTIES_ENCODE_PRETTY);
+}
+```
+
+Will output the following JSON (order of keys can differ):
+
+```JSON
+{
+ "array/doubleArr": [
+ 1.0,
+ 2.0
+ ],
+ "array/boolArr": [
+ true,
+ false
+ ],
+ "single/versionKey": "version<1.2.3.qualifier>",
+ "array/longArr": [
+ 1,
+ 2
+ ],
+ "single/strKey": "strValue",
+ "single/doubleKey": 2.0,
+ "single/boolKey": true,
+ "array/versionArr": [
+ "version<1.2.3.qualifier>",
+ "version<4.5.6.qualifier>"
+ ],
+ "array/stringArr": [
+ "value1",
+ "value2"
+ ],
+ "single/longKey": 42
+}
+```
+
+### Properties Nested Encoding
+
+When properties are encoded to JSON in a nested structure, the keys of the properties are used to create a nested JSON
+object. This is done by using the '/' character in the properties key to create a nested JSON objects. When encoding
+properties using a nested structure, there is a risk of key collisions. To detect key collisions, the flag
+`CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS` can be used.
+
+Nested Encoding example:
+
+```C
+#include <stdio.h>
+#include <celix/properties.h>
+
+int main() {
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+ celix_properties_set(props, "single/strKey", "strValue");
+ celix_properties_setLong(props, "single/longKey", 42);
+ celix_properties_setDouble(props, "single/doubleKey", 2.0);
+ celix_properties_setBool(props, "single/boolKey", true);
+ celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier"));
+
+ celix_array_list_t* strArr = celix_arrayList_createStringArray();
+ celix_arrayList_addString(strArr, "value1");
+ celix_arrayList_addString(strArr, "value2");
+ celix_properties_assignArrayList(props, "array/stringArr", strArr);
+
+ celix_array_list_t* longArr = celix_arrayList_createLongArray();
+ celix_arrayList_addLong(longArr, 1);
+ celix_arrayList_addLong(longArr, 2);
+ celix_properties_assignArrayList(props, "array/longArr", longArr);
+
+ celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray();
+ celix_arrayList_addDouble(doubleArr, 1.0);
+ celix_arrayList_addDouble(doubleArr, 2.0);
+ celix_properties_assignArrayList(props, "array/doubleArr", doubleArr);
+
+ celix_array_list_t* boolArr = celix_arrayList_createBoolArray();
+ celix_arrayList_addBool(boolArr, true);
+ celix_arrayList_addBool(boolArr, false);
+ celix_properties_assignArrayList(props, "array/boolArr", boolArr);
+
+ celix_array_list_t* versionArr = celix_arrayList_createVersionArray();
+ celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier"));
+ celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier"));
+ celix_properties_assignArrayList(props, "array/versionArr", versionArr);
+
+ celix_properties_saveToStream(props, stdout, CELIX_PROPERTIES_ENCODE_PRETTY | CELIX_PROPERTIES_ENCODE_NESTED_STYLE);
+}
+```
+
+Will output the following JSON (order of keys can differ):
+
+```JSON
+{
+ "array": {
+ "doubleArr": [
+ 1.0,
+ 2.0
+ ],
+ "boolArr": [
+ true,
+ false
+ ],
+ "longArr": [
+ 1,
+ 2
+ ],
+ "versionArr": [
+ "version<1.2.3.qualifier>",
+ "version<4.5.6.qualifier>"
+ ],
+ "stringArr": [
+ "value1",
+ "value2"
+ ]
+ },
+ "single": {
+ "versionKey": "version<1.2.3.qualifier>",
+ "strKey": "strValue",
+ "doubleKey": 2.0,
+ "boolKey": true,
+ "longKey": 42
+ }
+}
+```
+
+### Encoding Flags
+
+Properties encoding flags can be used control the behavior of the encoding. The following encoding flags can be used:
+
+- `CELIX_PROPERTIES_ENCODE_PRETTY`: Flag to indicate that the encoded output should be pretty; e.g. encoded with
+ additional whitespaces, newlines and indentation. If this flag is not set, the encoded output will compact; e.g.
+ without additional whitespaces, newlines and indentation.
+
+- `CELIX_PROPERTIES_ENCODE_FLAT_STYLE`: Flag to indicate that the encoded output should be flat; e.g. all properties
+ entries are written as top level field entries.
+
+- `CELIX_PROPERTIES_ENCODE_NESTED_STYLE`: Flag to indicate that the encoded output should be nested; e.g. properties
+ entries are split on '/' and nested in JSON objects.
+
+- `CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS`: Flag to indicate that the encoding should fail if the JSON
+ representation will contain colliding keys. Note that colliding keys can only occur when using the nested encoding
+ style.
+
+- `CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS`: Flag to indicate that the encoding should fail if the JSON
+ representation will contain empty arrays.
+
+- `CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF`: Flag to indicate that the encoding should fail if the JSON representation
+ will contain NaN or Inf values.
+
+- `CELIX_PROPERTIES_ENCODE_STRICT`: Flag to indicate that all encode "error on" flags should be set.
+
+## Properties Decoding
+
+JSON can be decoded to an Apache Celix properties object using
+the `celix_properties_load2`, `celix_properties_loadFromStream` and `celix_properties_loadFromString2` functions. These
+functions take a JSON input and decode it to a properties object. Because properties use a flat key structure,
+decoding a nested JSON object to properties results in combining JSON object keys to a flat key structure. This can
+result in key collisions.
+
+By default, the decoding will not fail on empty arrays, null values, empty keys, or mixed arrays and instead these JSON
+entries will be ignored. Also by default, if decoding results in a duplicate properties key, the last value will be used
+and no error will be returned.
+
+### Decoding example
+
+Given a `example.json` file with the following content:
+
+```JSON
+{
+ "counters": {
+ "counter1": 1,
+ "counter2": 2
+ },
+ "strings": {
+ "string1": "value1",
+ "string2": "value2"
+ }
+}
+```
+
+Combined with the following code:
+
+```c
+#include <stdio.h>
+
+#include <celix/properties.h>
+
+int main() {
+ celix_autoptr(celix_properties_t) props;
+ celix_status_t status = celix_properties_load2("example.json", 0, &props):
+ (void)status; //for production code check status
+ CELIX_PROPERTIES_ITERATE(props, iter) {
+ printf("key=%s, value=%s\n", celix_properties_key(iter.key), celix_properties_value(iter.entry.value));
+ }
+}
+```
+
+Will output the following:
+
+```
+key=counters/counter1, value=1
+key=counters/counter2, value=2
+key=strings/string1, value=value1
+key=strings/string2, value=value2
+```
+
+### Decoding Flags
+
+Properties decoding behavior can be controlled using flags. The following decoding flags can be used:
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES`: Flag to indicate that the decoding should fail if the input contains
+ duplicate JSON keys.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS`: Flag to indicate that the decoding should fail if the input contains
+ entry that collide on property keys.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES`: Flag to indicate that the decoding should fail if the input contains
+ null values.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS`: Flag to indicate that the decoding should fail if the input contains
+ empty arrays.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS`: Flag to indicate that the decoding should fail if the input contains
+ empty keys.
+
+- `CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS`: Flag to indicate that the decoding should fail if the input contains
+ mixed arrays.
+
+- `CELIX_PROPERTIES_DECODE_STRICT`: Flag to indicate that the decoding should fail if the input contains any of the
+ decode error flags.
diff --git a/libs/error_injector/jansson/CMakeLists.txt b/libs/error_injector/jansson/CMakeLists.txt
index b536956..ce4beaa 100644
--- a/libs/error_injector/jansson/CMakeLists.txt
+++ b/libs/error_injector/jansson/CMakeLists.txt
@@ -35,5 +35,8 @@
LINKER:--wrap,json_integer
LINKER:--wrap,json_string
LINKER:--wrap,json_real
+ LINKER:--wrap,json_vsprintf
+ LINKER:--wrap,json_sprintf
+ LINKER:--wrap,json_dumpf
)
add_library(Celix::jansson_ei ALIAS jansson_ei)
diff --git a/libs/error_injector/jansson/include/jansson_ei.h b/libs/error_injector/jansson/include/jansson_ei.h
index 60f04e4..b98bd88 100644
--- a/libs/error_injector/jansson/include/jansson_ei.h
+++ b/libs/error_injector/jansson/include/jansson_ei.h
@@ -34,6 +34,9 @@
CELIX_EI_DECLARE(json_integer, json_t*);
CELIX_EI_DECLARE(json_string, json_t*);
CELIX_EI_DECLARE(json_real, json_t*);
+CELIX_EI_DECLARE(json_vsprintf,json_t*);
+CELIX_EI_DECLARE(json_sprintf, json_t*);
+CELIX_EI_DECLARE(json_dumpf, int);
#ifdef __cplusplus
}
diff --git a/libs/error_injector/jansson/src/jansson_ei.cc b/libs/error_injector/jansson/src/jansson_ei.cc
index 57033c0..98289c9 100644
--- a/libs/error_injector/jansson/src/jansson_ei.cc
+++ b/libs/error_injector/jansson/src/jansson_ei.cc
@@ -23,69 +23,94 @@
extern "C" {
-size_t __real_json_array_size(const json_t *array);
+size_t __real_json_array_size(const json_t* array);
CELIX_EI_DEFINE(json_array_size, size_t)
-size_t __wrap_json_array_size(const json_t *array) {
+size_t __wrap_json_array_size(const json_t* array) {
CELIX_EI_IMPL(json_array_size);
return __real_json_array_size(array);
}
-char *__real_json_dumps(const json_t *json, size_t flags);
+char* __real_json_dumps(const json_t* json, size_t flags);
CELIX_EI_DEFINE(json_dumps, char*)
-char *__wrap_json_dumps(const json_t *json, size_t flags) {
+char* __wrap_json_dumps(const json_t* json, size_t flags) {
CELIX_EI_IMPL(json_dumps);
return __real_json_dumps(json, flags);
}
-json_t *__real_json_object(void);
+json_t* __real_json_object(void);
CELIX_EI_DEFINE(json_object, json_t*)
-json_t *__wrap_json_object(void) {
+json_t* __wrap_json_object(void) {
CELIX_EI_IMPL(json_object);
return __real_json_object();
}
-int __real_json_object_set_new(json_t *object, const char *key, json_t *value);
+int __real_json_object_set_new(json_t* object, const char* key, json_t* value);
CELIX_EI_DEFINE(json_object_set_new, int)
-int __wrap_json_object_set_new(json_t *object, const char *key, json_t *value) {
- json_auto_t *val = value;
+int __wrap_json_object_set_new(json_t* object, const char* key, json_t* value) {
+ json_auto_t* val = value;
CELIX_EI_IMPL(json_object_set_new);
return __real_json_object_set_new(object, key, celix_steal_ptr(val));
}
-json_t *__real_json_array(void);
+json_t* __real_json_array(void);
CELIX_EI_DEFINE(json_array, json_t*)
-json_t *__wrap_json_array(void) {
+json_t* __wrap_json_array(void) {
CELIX_EI_IMPL(json_array);
return __real_json_array();
}
-int __real_json_array_append_new(json_t *array, json_t *value);
+int __real_json_array_append_new(json_t* array, json_t* value);
CELIX_EI_DEFINE(json_array_append_new, int)
-int __wrap_json_array_append_new(json_t *array, json_t *value) {
- json_auto_t *val = value;
+int __wrap_json_array_append_new(json_t* array, json_t* value) {
+ json_auto_t* val = value;
CELIX_EI_IMPL(json_array_append_new);
return __real_json_array_append_new(array, celix_steal_ptr(val));
}
-json_t *__real_json_integer(json_int_t value);
+json_t* __real_json_integer(json_int_t value);
CELIX_EI_DEFINE(json_integer, json_t*)
-json_t *__wrap_json_integer(json_int_t value) {
+json_t* __wrap_json_integer(json_int_t value) {
CELIX_EI_IMPL(json_integer);
return __real_json_integer(value);
}
-json_t *__real_json_string(const char *value);
+json_t* __real_json_string(const char* value);
CELIX_EI_DEFINE(json_string, json_t*)
-json_t *__wrap_json_string(const char *value) {
+json_t* __wrap_json_string(const char* value) {
CELIX_EI_IMPL(json_string);
return __real_json_string(value);
}
-json_t *__real_json_real(double value);
+json_t* __real_json_real(double value);
CELIX_EI_DEFINE(json_real, json_t*)
-json_t *__wrap_json_real(double value) {
+json_t* __wrap_json_real(double value) {
CELIX_EI_IMPL(json_real);
return __real_json_real(value);
}
+json_t* __real_json_vsprintf(const char* fmt, va_list ap);
+CELIX_EI_DEFINE(json_vsprintf, json_t*)
+json_t* __wrap_json_vsprintf(const char* fmt, va_list ap) {
+ CELIX_EI_IMPL(json_vsprintf);
+ return __real_json_vsprintf(fmt, ap);
+}
+
+json_t* __real_json_sprintf(const char* fmt, ...);
+CELIX_EI_DEFINE(json_sprintf, json_t*)
+json_t* __wrap_json_sprintf(const char* fmt, ...) {
+ CELIX_EI_IMPL(json_sprintf);
+ va_list args;
+ va_start(args, fmt);
+ json_t* obj = __real_json_vsprintf(fmt, args);
+ va_end(args);
+ return obj;
+}
+
+int __real_json_dumpf(const json_t* json, FILE* output, size_t flags);
+CELIX_EI_DEFINE(json_dumpf, int)
+int __wrap_json_dumpf(const json_t* json, FILE* output, size_t flags) {
+ CELIX_EI_IMPL(json_dumpf);
+ return __real_json_dumpf(json, output, flags);
+}
+
}
\ No newline at end of file
diff --git a/libs/framework/include/celix/FrameworkExceptions.h b/libs/framework/include/celix/FrameworkExceptions.h
index d6fd2cd..14b35a2 100644
--- a/libs/framework/include/celix/FrameworkExceptions.h
+++ b/libs/framework/include/celix/FrameworkExceptions.h
@@ -18,7 +18,7 @@
*/
#pragma once
-#include "celix/Exception.h"
+#include "celix/Exceptions.h"
namespace celix {
diff --git a/libs/framework/include/celix/ScheduledEventBuilder.h b/libs/framework/include/celix/ScheduledEventBuilder.h
index c93a93b..47cef48 100644
--- a/libs/framework/include/celix/ScheduledEventBuilder.h
+++ b/libs/framework/include/celix/ScheduledEventBuilder.h
@@ -22,8 +22,8 @@
#include <memory>
#include <functional>
+#include "celix/Exceptions.h"
#include "celix/ScheduledEvent.h"
-#include "celix/Exception.h"
namespace celix {
@@ -112,7 +112,7 @@
*/
ScheduledEvent build() {
if (!callback) {
- throw celix::Exception{"Cannot build scheduled event without callback"}; //TODO improve error
+ throw celix::Exception{"Cannot build scheduled event without callback"};
}
return ScheduledEvent{ctx, name, std::move(callback), std::move(removeCallback), options};
}
diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt
index e1b4983..cec80f4 100644
--- a/libs/utils/CMakeLists.txt
+++ b/libs/utils/CMakeLists.txt
@@ -18,6 +18,7 @@
celix_subproject(UTILS "Option to enable building the Utilities library" ON)
if (UTILS)
find_package(libzip REQUIRED)
+ find_package(jansson REQUIRED) #TODO add jansson dep info to build (conan) and documentation info
set(MEMSTREAM_SOURCES )
set(MEMSTREAM_INCLUDES )
@@ -29,6 +30,7 @@
src/version.c
src/version_range.c
src/properties.c
+ src/properties_encoding.c
src/utils.c
src/filter.c
src/celix_log_level.c
@@ -41,7 +43,7 @@
src/celix_cleanup.c
${MEMSTREAM_SOURCES}
)
- set(UTILS_PRIVATE_DEPS libzip::zip)
+ set(UTILS_PRIVATE_DEPS libzip::zip jansson::jansson)
set(UTILS_PUBLIC_DEPS)
add_library(utils SHARED ${UTILS_SRC})
diff --git a/libs/utils/error_injector/celix_version/CMakeLists.txt b/libs/utils/error_injector/celix_version/CMakeLists.txt
index ed7aadc..81886a5 100644
--- a/libs/utils/error_injector/celix_version/CMakeLists.txt
+++ b/libs/utils/error_injector/celix_version/CMakeLists.txt
@@ -24,5 +24,6 @@
LINKER:--wrap,celix_version_createVersionFromString
LINKER:--wrap,celix_version_parse
LINKER:--wrap,celix_version_copy
+ LINKER:--wrap,celix_version_toString
)
add_library(Celix::version_ei ALIAS version_ei)
diff --git a/libs/utils/error_injector/celix_version/include/celix_version_ei.h b/libs/utils/error_injector/celix_version/include/celix_version_ei.h
index a823185..e5d510f 100644
--- a/libs/utils/error_injector/celix_version/include/celix_version_ei.h
+++ b/libs/utils/error_injector/celix_version/include/celix_version_ei.h
@@ -31,6 +31,8 @@
CELIX_EI_DECLARE(celix_version_copy, celix_version_t*);
+CELIX_EI_DECLARE(celix_version_toString, char*);
+
#ifdef __cplusplus
}
#endif
diff --git a/libs/utils/error_injector/celix_version/src/celix_version_ei.cc b/libs/utils/error_injector/celix_version/src/celix_version_ei.cc
index eefad50..b09339c 100644
--- a/libs/utils/error_injector/celix_version/src/celix_version_ei.cc
+++ b/libs/utils/error_injector/celix_version/src/celix_version_ei.cc
@@ -41,4 +41,11 @@
return __real_celix_version_copy(version);
}
-}
\ No newline at end of file
+char* __real_celix_version_toString(const celix_version_t* version);
+CELIX_EI_DEFINE(celix_version_toString, char*);
+char* __wrap_celix_version_toString(const celix_version_t* version) {
+ CELIX_EI_IMPL(celix_version_toString);
+ return __real_celix_version_toString(version);
+}
+
+}
diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt
index d04f9e3..0d717e1 100644
--- a/libs/utils/gtest/CMakeLists.txt
+++ b/libs/utils/gtest/CMakeLists.txt
@@ -29,6 +29,7 @@
src/CelixUtilsTestSuite.cc
src/ConvertUtilsTestSuite.cc
src/PropertiesTestSuite.cc
+ src/PropertiesEncodingTestSuite.cc
src/VersionTestSuite.cc
src/ErrTestSuite.cc
src/ThreadsTestSuite.cc
@@ -36,6 +37,7 @@
src/CelixUtilsAutoCleanupTestSuite.cc
src/ArrayListTestSuite.cc
src/DeprecatedHashmapTestSuite.cc
+ src/CxxExceptionsTestSuite.cc
)
target_link_libraries(test_utils PRIVATE utils_cut Celix::utils GTest::gtest GTest::gtest_main libzip::zip)
@@ -104,6 +106,19 @@
add_test(NAME test_utils_celix_err_with_ei COMMAND test_utils_celix_err_with_ei)
setup_target_for_coverage(test_utils_celix_err_with_ei SCAN_DIR ..)
+ #Note testing version seperated, otherwise version calls are already wrapped
+ add_executable(test_utils_version_with_ei
+ src/VersionErrorInjectionTestSuite.cc
+ )
+ target_link_libraries(test_utils_version_with_ei PRIVATE
+ utils_cut
+ Celix::malloc_ei
+ Celix::asprintf_ei
+ Celix::utils_ei
+ GTest::gtest GTest::gtest_main
+ )
+ add_test(NAME test_utils_version_with_ei COMMAND test_utils_version_with_ei)
+ setup_target_for_coverage(test_utils_version_with_ei SCAN_DIR ..)
#Note testing array list seperated, otherwise array list calls are already wrapped
add_executable(test_utils_array_list_with_ei
@@ -123,9 +138,9 @@
src/FileUtilsErrorInjectionTestSuite.cc
src/ConvertUtilsErrorInjectionTestSuite.cc
src/PropertiesErrorInjectionTestSuite.cc
- src/VersionErrorInjectionTestSuite.cc
src/HashMapErrorInjectionTestSuite.cc
src/FilterErrorInjectionTestSuite.cc
+ src/PropertiesEncodingErrorInjectionTestSuite.cc
)
target_link_libraries(test_utils_with_ei PRIVATE
Celix::zip_ei
@@ -142,6 +157,7 @@
Celix::long_hash_map_ei
Celix::version_ei
Celix::array_list_ei
+ Celix::jansson_ei
GTest::gtest GTest::gtest_main
)
target_include_directories(test_utils_with_ei PRIVATE ../src) #for version_private (needs refactoring of test)
diff --git a/libs/utils/gtest/src/CelixUtilsTestSuite.cc b/libs/utils/gtest/src/CelixUtilsTestSuite.cc
index 26bef64..95e143d 100644
--- a/libs/utils/gtest/src/CelixUtilsTestSuite.cc
+++ b/libs/utils/gtest/src/CelixUtilsTestSuite.cc
@@ -315,6 +315,38 @@
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/CxxExceptionsTestSuite.cc b/libs/utils/gtest/src/CxxExceptionsTestSuite.cc
new file mode 100644
index 0000000..9ff0742
--- /dev/null
+++ b/libs/utils/gtest/src/CxxExceptionsTestSuite.cc
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "celix/Exceptions.h"
+
+class ExceptionsTestSuite : public ::testing::Test {
+public:
+};
+
+
+TEST_F(ExceptionsTestSuite, ThrowExceptionTest) {
+ EXPECT_THROW(celix::impl::throwException(CELIX_ILLEGAL_ARGUMENT, "Test"), celix::IllegalArgumentException);
+ try {
+ celix::impl::throwException(CELIX_ILLEGAL_ARGUMENT, "Test");
+ } catch (const celix::IllegalArgumentException& ex) {
+ EXPECT_STREQ("Test (Illegal argument)", ex.what());
+ }
+
+ EXPECT_THROW(celix::impl::throwException(CELIX_FILE_IO_EXCEPTION, "Test"), celix::IOException);
+ try {
+ celix::impl::throwException(CELIX_FILE_IO_EXCEPTION, "Test");
+ } catch (const celix::IOException& ex) {
+ EXPECT_STREQ("Test (File I/O exception)", ex.what());
+ }
+
+ //Not all celix_status_t values are mapped and in that case the default Exception is thrown
+ EXPECT_THROW(celix::impl::throwException(CELIX_FRAMEWORK_EXCEPTION, "Test"), celix::Exception);
+ try {
+ celix::impl::throwException(CELIX_FRAMEWORK_EXCEPTION, "Test");
+ } catch (const celix::Exception& ex) {
+ EXPECT_STREQ("Test (Framework exception)", ex.what());
+ }
+}
+
diff --git a/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc
new file mode 100644
index 0000000..9a5c15f
--- /dev/null
+++ b/libs/utils/gtest/src/PropertiesEncodingErrorInjectionTestSuite.cc
@@ -0,0 +1,385 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "celix_properties.h"
+#include "celix/Properties.h"
+
+#include "celix_err.h"
+#include "celix_array_list_ei.h"
+#include "celix_utils_ei.h"
+#include "celix_version_ei.h"
+#include "jansson_ei.h"
+#include "malloc_ei.h"
+#include "stdio_ei.h"
+
+class PropertiesEncodingErrorInjectionTestSuite : public ::testing::Test {
+ public:
+ PropertiesEncodingErrorInjectionTestSuite() = default;
+
+ ~PropertiesEncodingErrorInjectionTestSuite() override {
+ celix_ei_expect_json_object(nullptr, 0, nullptr);
+ celix_ei_expect_open_memstream(nullptr, 0, nullptr);
+ celix_ei_expect_celix_utils_writeOrCreateString(nullptr, 0, nullptr);
+ celix_ei_expect_json_object_set_new(nullptr, 0, -1);
+ celix_ei_expect_json_sprintf(nullptr, 0, nullptr);
+ celix_ei_expect_celix_version_toString(nullptr, 0, nullptr);
+ celix_ei_expect_malloc(nullptr, 0, nullptr);
+ celix_ei_expect_celix_arrayList_createWithOptions(nullptr, 0, nullptr);
+ celix_ei_expect_celix_arrayList_addString(nullptr, 0, CELIX_SUCCESS);
+ celix_err_resetErrors();
+ }
+};
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, SaveErrorTest) {
+ //Given a dummy properties object
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "key", "value");
+
+ //When an error injected is prepared for json_object() from saveToStream
+ celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr);
+
+ //And a dummy stream is created
+ FILE* stream = fopen("/dev/null", "w");
+
+ //When I call celix_properties_saveToStream
+ celix_status_t status = celix_properties_saveToStream(props, stream, 0);
+
+ //Then I expect an error
+ EXPECT_EQ(CELIX_ENOMEM, status);
+ fclose(stream);
+
+ //When an error injected is prepared for open_memstream()n from save
+ celix_ei_expect_open_memstream((void*)celix_properties_saveToString, 0, nullptr);
+
+ //When I call celix_properties_saveToString
+ char* out;
+ status = celix_properties_saveToString(props, 0, &out);
+
+ //Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ //And I expect 2 error messages in celix_err
+ EXPECT_EQ(2, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, FcloseErrorWhenSaveTest) {
+ //Given a dummy properties object
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "key", "value");
+
+ //When an error injected is prepared for fclose() from save
+ celix_ei_expect_fclose((void*)celix_properties_save, 0, -1);
+
+ //And I call celix_properties_save
+ auto status = celix_properties_save(props, "somefile.json", 0);
+
+ //Then I expect an error
+ EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status);
+
+ //And I expect 1 error message in celix_err
+ EXPECT_EQ(1, celix_err_getErrorCount());
+}
+
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeErrorTest) {
+ // Given a dummy properties object
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "key.with.slash", "value");
+ celix_properties_set(props, "key-with-out-slash", "value");
+
+ // When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_saveToString
+ celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_saveToString, 2, nullptr);
+
+ // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge)
+ char* out;
+ auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // When an error injected is prepared for json_object() from celix_properties_saveToString
+ celix_ei_expect_json_object((void*)celix_properties_saveToString, 2, nullptr);
+
+ // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge)
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // When an error injected is prepared for json_object_set_new() from celix_properties_saveToString
+ celix_ei_expect_json_object_set_new((void*)celix_properties_saveToString, 2, -1);
+
+ // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge)
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // When an error injected is prepared for json_string() from celix_properties_saveToString
+ celix_ei_expect_json_string((void*)celix_properties_saveToString, 3, nullptr);
+
+ // And I call celix_properties_saveToString using NESTED encoding (whitebox-knowledge)
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // When an error injected is prepared for json_object_set_new() from celix_properties_saveToString
+ celix_ei_expect_json_object_set_new((void*)celix_properties_saveToString, 3, -1);
+
+ // And I call celix_properties_saveToString using FLAT encoding (whitebox-knowledge)
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_FLAT_STYLE, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // And I expect 5 error message in celix_err
+ EXPECT_EQ(5, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeArrayErrorTest) {
+ // Given a dummy properties object with an array
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ auto* arr = celix_arrayList_createStringArray();
+ celix_arrayList_addString(arr, "value1");
+ celix_arrayList_addString(arr, "value2");
+ celix_properties_assignArrayList(props, "key", arr);
+
+ // When an error injected is prepared for json_array() from celix_properties_saveToString
+ celix_ei_expect_json_array((void*)celix_properties_saveToString, 4, nullptr);
+
+ // And I call celix_properties_saveToString
+ char* out;
+ auto status = celix_properties_saveToString(props, 0, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+
+ //When an error injected is prepared for json_array_append_new() from loadFromString2
+ celix_ei_expect_json_array_append_new((void*)celix_properties_saveToString, 4, -1);
+
+ //And I call celix_properties_saveToString
+ status = celix_properties_saveToString(props, 0, &out);
+
+ //Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ //When an error injected is prepared for json_string() from loadFromString2
+ celix_ei_expect_json_string((void*)celix_properties_saveToString, 5, nullptr);
+
+ //And I call celix_properties_saveToString
+ status = celix_properties_saveToString(props, 0, &out);
+
+ //Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // And I expect 3 error message in celix_err
+ EXPECT_EQ(4, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeVersionErrorTest) {
+ // Given a dummy properties object with a version
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ auto* version = celix_version_create(1, 2, 3, "qualifier");
+ celix_properties_assignVersion(props, "key", version);
+
+ // When an error injected is prepared for json_sprintf() from celix_properties_saveToString
+ celix_ei_expect_json_sprintf((void*)celix_properties_saveToString, 4, nullptr);
+
+ // And I call celix_properties_saveToString
+ char* out;
+ auto status = celix_properties_saveToString(props, 0, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // When an error injected is prepared for celix_version_toString() from celix_properties_saveToString
+ celix_ei_expect_celix_version_toString((void*)celix_properties_saveToString, 4, nullptr);
+
+ // And I call celix_properties_saveToString
+ status = celix_properties_saveToString(props, 0, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // And I expect 2 error message in celix_err
+ EXPECT_EQ(2, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, EncodeDumpfErrorTest) {
+ // Given a dummy properties object
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "key", "value");
+
+ // When an error injected is prepared for json_dumpf() from celix_properties_saveToString
+ celix_ei_expect_json_dumpf((void*)celix_properties_saveToStream, 0, -1);
+
+ // And I call celix_properties_saveToString
+ char* out;
+ auto status = celix_properties_saveToString(props, 0, &out);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // And I expect 1 error message in celix_err
+ EXPECT_EQ(1, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, LoadErrorTest) {
+ //Given a dummy json string
+ const char* json = R"({"key":"value"})";
+
+ //When an error injected is prepared for fmemopen() from loadFromString2
+ celix_ei_expect_fmemopen((void*)celix_properties_loadFromString2, 0, nullptr);
+
+ //When I call celix_properties_loadFromString
+ celix_properties_t* props;
+ auto status = celix_properties_loadFromString2(json, 0, &props);
+
+ //Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ //And I expect 1 error message in celix_err
+ EXPECT_EQ(1, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeErrorTest) {
+ //Given a dummy json string
+ const char* json = R"({"key":"value", "object": {"key":"value"}})";
+
+ //When an error injected is prepared for celix_properties_create()->malloc() from celix_properties_loadFromString2
+ celix_ei_expect_malloc((void*)celix_properties_loadFromString2, 3, nullptr);
+
+ //When I call celix_properties_loadFromString
+ celix_properties_t* props;
+ auto status = celix_properties_loadFromString2(json, 0, &props);
+
+ //Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ //When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_loadFromString2
+ celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_loadFromString2, 3, nullptr);
+
+ //When I call celix_properties_loadFromString
+ status = celix_properties_loadFromString2(json, 0, &props);
+
+ //Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ //And I expect 2 error message in celix_err
+ EXPECT_EQ(2, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeArrayErrorTest) {
+ //Given a dummy json string
+ const char* json = R"({"key":["value1", "value2"]})";
+
+ // When an error injected is prepared for celix_arrayList_createWithOptions() from celix_properties_loadFromString2
+ celix_ei_expect_celix_arrayList_createWithOptions((void*)celix_properties_loadFromString2, 4, nullptr);
+
+ //When I call celix_properties_loadFromString
+ celix_properties_t* props;
+ auto status = celix_properties_loadFromString2(json, 0, &props);
+
+ //Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // When an error injected is prepared for celix_arrayList_addString() from celix_properties_loadFromString2
+ celix_ei_expect_celix_arrayList_addString((void*)celix_properties_loadFromString2, 4, ENOMEM);
+
+ //When I call celix_properties_loadFromString
+ status = celix_properties_loadFromString2(json, 0, &props);
+
+ //Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // And I expect 0 error message in celix_err. Note because errors are injected for celix_array_list_t, celix_err is
+ // not used
+ EXPECT_EQ(0, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, DecodeVersionErrorTest) {
+ // Given a dummy json version string
+ const char* json = R"({"key":"version<1.2.3.qualifier>"})";
+
+ // When an error injected is prepared for celix_utils_writeOrCreateString() from celix_properties_loadFromString2
+ celix_ei_expect_celix_utils_writeOrCreateString((void*)celix_properties_loadFromString2, 4, nullptr);
+
+ // And I call celix_properties_loadFromString
+ celix_properties_t* props;
+ auto status = celix_properties_loadFromString2(json, 0, &props);
+
+ // Then I expect an error
+ EXPECT_EQ(ENOMEM, status);
+
+ // And I expect 1 error message in celix_err
+ EXPECT_EQ(1, celix_err_getErrorCount());
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, SaveCxxPropertiesErrorTest) {
+ //Given a dummy Properties object
+ celix::Properties props{};
+ props.set("key", "value");
+
+ //When an error injected is prepared for json_object() from saveToStream
+ celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr);
+
+ //Then saving to file throws a bad alloc exception
+ EXPECT_THROW(props.save("somefile.json"), std::bad_alloc);
+
+ //When an error injected is prepared for json_object() from saveToStream
+ celix_ei_expect_json_object((void*)celix_properties_saveToStream, 0, nullptr);
+
+ //Then saving to string throws a bad alloc exception
+ EXPECT_THROW(props.saveToString(), std::bad_alloc);
+}
+
+TEST_F(PropertiesEncodingErrorInjectionTestSuite, LoadCxxPropertiesErrorTest) {
+ //Given a dummy json string
+ const char* json = R"({"key":"value"})";
+
+ //When an error injected is prepared for malloc() from celix_properties_create
+ celix_ei_expect_malloc((void*)celix_properties_create, 0, nullptr);
+
+ //Then loading from string throws a bad alloc exception
+ EXPECT_THROW(celix::Properties::loadFromString(json), std::bad_alloc);
+
+ //When an error injected is prepared for malloc() from celix_properties_create
+ celix_ei_expect_malloc((void*)celix_properties_create, 0, nullptr);
+
+ //And an empty json file exists
+ FILE* file = fopen("empty.json", "w");
+ fprintf(file, "{}");
+ fclose(file);
+
+ //Then loading from file throws a bad alloc exception
+ EXPECT_THROW(celix::Properties::load2("empty.json"), std::bad_alloc);
+}
diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
new file mode 100644
index 0000000..fedec72
--- /dev/null
+++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
@@ -0,0 +1,1161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+#include <cmath>
+#include <jansson.h>
+
+#include "celix/Properties.h"
+#include "celix_err.h"
+#include "celix_properties.h"
+#include "celix_properties_private.h"
+#include "celix_stdlib_cleanup.h"
+
+
+class PropertiesSerializationTestSuite : public ::testing::Test {
+ public:
+ PropertiesSerializationTestSuite() { celix_err_resetErrors(); }
+};
+
+TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) {
+ //Given an empty properties object
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+ //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 the stream
+ auto status = celix_properties_saveToStream(props, stream, 0);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ //Then the stream contains an empty JSON object
+ fclose(stream);
+ EXPECT_STREQ("{}", buf);
+}
+
+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");
+ celix_properties_set(props, "key2", "value2");
+ celix_properties_setLong(props, "key3", 3);
+ celix_properties_setDouble(props, "key4", 4.0);
+ celix_properties_setBool(props, "key5", true);
+ celix_properties_assignVersion(props, "key6", celix_version_create(1, 2, 3, "qualifier"));
+
+ //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 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
+ fclose(stream);
+ EXPECT_NE(nullptr, strstr(buf, R"("key1":"value1")")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key2":"value2")")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key3":3)")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key4":4.0)")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key5":true)")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key6":"version<1.2.3.qualifier>")")) << "JSON: " << buf;
+
+ //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, SavePropertiesWithNaNAndInfValuesTest) {
+ //Given a NAN, INF and -INF value
+ auto keys = {"NAN", "INF", "-INF"};
+ for (const auto& key : keys) {
+ //For every value
+
+ //Given a properties object with a NAN, INF or -INF value
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_setDouble(props, key, strtod(key, nullptr));
+
+ // Then saving the properties to a string succeeds, but value is not added to the JSON (because JSON does not
+ // support NAN, INF and -INF)
+ celix_autofree char* output;
+ auto status = celix_properties_saveToString(props, 0, &output);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+ EXPECT_STREQ("{}", output);
+
+ //And saving the properties to a string with the flag CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF fails
+ celix_err_resetErrors();
+ char* output2;
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, &output2);
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+ //And an error msg is added to celix_err
+ EXPECT_EQ(1, celix_err_getErrorCount());
+ }
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsContainingNaNAndInfValueTest) {
+ auto keys = {"NAN", "INF", "-INF"};
+ for (const auto& key : keys) {
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_autoptr(celix_array_list_t) list = celix_arrayList_createDoubleArray();
+ celix_arrayList_addDouble(list, strtod(key, nullptr));
+ celix_properties_assignArrayList(props, key, celix_steal_ptr(list));
+
+ // Then saving the properties to a string succeeds, but value is not added to the JSON (because JSON does not
+ // support NAN, INF and -INF)
+ celix_autofree char* output;
+ auto status = celix_properties_saveToString(props, 0, &output);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+ EXPECT_STREQ("{}", output);
+
+ //And saving the properties to a string with the flag CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF fails
+ celix_err_resetErrors();
+ char* output2;
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, &output2);
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+ //And an error msg is added to celix_err
+ EXPECT_EQ(2, celix_err_getErrorCount());
+
+ celix_err_resetErrors();
+ char* output3;
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output3);
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+ EXPECT_EQ(1, celix_err_getErrorCount());
+ }
+}
+
+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();
+ celix_arrayList_addString(list1, "value1");
+ celix_arrayList_addString(list1, "value2");
+ celix_properties_assignArrayList(props, "key1", list1);
+
+ celix_array_list_t* list2 = celix_arrayList_createLongArray();
+ celix_arrayList_addLong(list2, 1);
+ celix_arrayList_addLong(list2, 2);
+ celix_properties_assignArrayList(props, "key2", list2);
+
+ celix_array_list_t* list3 = celix_arrayList_createDoubleArray();
+ celix_arrayList_addDouble(list3, 1.0);
+ celix_arrayList_addDouble(list3, 2.0);
+ celix_properties_assignArrayList(props, "key3", list3);
+
+ celix_array_list_t* list4 = celix_arrayList_createBoolArray();
+ celix_arrayList_addBool(list4, true);
+ celix_arrayList_addBool(list4, false);
+ celix_properties_assignArrayList(props, "key4", list4);
+
+ celix_array_list_t* list5 = celix_arrayList_createVersionArray();
+ celix_arrayList_assignVersion(list5, celix_version_create(1, 2, 3, "qualifier"));
+ celix_arrayList_assignVersion(list5, celix_version_create(4, 5, 6, "qualifier"));
+ celix_properties_assignArrayList(props, "key5", list5);
+
+ // 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 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
+ fclose(stream);
+ EXPECT_NE(nullptr, strstr(buf, R"("key1":["value1","value2"])")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key2":[1,2])")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key3":[1.0,2.0])")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key4":[true,false])")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key5":["version<1.2.3.qualifier>","version<4.5.6.qualifier>"])"))
+ << "JSON: " << buf;
+
+ // 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, 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());
+ celix_properties_assignArrayList(props, "key2", celix_arrayList_createLongArray());
+ celix_properties_assignArrayList(props, "key3", celix_arrayList_createDoubleArray());
+ celix_properties_assignArrayList(props, "key4", celix_arrayList_createBoolArray());
+ celix_properties_assignArrayList(props, "key5", celix_arrayList_createVersionArray());
+ EXPECT_EQ(5, celix_properties_size(props));
+
+ //When saving the properties to a string
+ celix_autofree char* output1;
+ auto status = celix_properties_saveToString(props, 0, &output1);
+
+ //Then the save went ok
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ //And the output contains an empty JSON object, because empty arrays are treated as unset
+ EXPECT_STREQ("{}", output1);
+
+ //When saving the properties to a string with an error on empty array flag
+ char* output2;
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output2);
+
+ //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, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveEmptyKeyTest) {
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_setString(props, "", "value");
+
+ celix_autofree char* output1;
+ auto status = celix_properties_saveToString(props, 0, &output1);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ celix_autoptr(celix_properties_t) prop2 = nullptr;
+ status = celix_properties_loadFromString2(output1, 0, &prop2);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ ASSERT_TRUE(celix_properties_equals(props, prop2));
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveJSONPathKeysTest) {
+ //Given a properties object with jpath keys
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "key1", "value1");
+ celix_properties_set(props, "key2", "value2");
+ celix_properties_set(props, "object1.key3", "value3");
+ celix_properties_set(props, "object1.key4", "value4");
+ celix_properties_set(props, "object2.key5", "value5");
+ celix_properties_set(props, "object3.object4.key6", "value6");
+
+ //And an in-memory stream
+ celix_autofree char* output;
+
+ //When saving the properties to the stream
+ auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ //Then the stream contains the JSON representation snippets of the properties
+ EXPECT_NE(nullptr, strstr(output, R"("key1":"value1")")) << "JSON: " << output;
+ EXPECT_NE(nullptr, strstr(output, R"("key2":"value2")")) << "JSON: " << output;
+ EXPECT_NE(nullptr, strstr(output, R"("object1":{"key3":"value3","key4":"value4"})")) << "JSON: " << output;
+ EXPECT_NE(nullptr, strstr(output, R"("object2":{"key5":"value5"})")) << "JSON: " << output;
+ EXPECT_NE(nullptr, strstr(output, R"("object3":{"object4":{"key6":"value6"}})")) << "JSON: " << output;
+
+ //And the buf is a valid JSON object
+ json_error_t error;
+ json_auto_t* root = json_loads(output, 0, &error);
+ EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+}
+
+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/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
+
+ //When saving the properties to a string
+ celix_autofree char* output = nullptr;
+ auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ //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(output, 0, &error);
+ EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+ json_decref(root);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNestedEndErrorOnCollisionsFlagsTest) {
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "key1", "value1");
+ celix_properties_set(props, "key2", "value2");
+ celix_properties_set(props, "object1.key3", "value3");
+ celix_properties_set(props, "object1.key4", "value4");
+ celix_properties_set(props, "object2.key5", "value5");
+ celix_properties_set(props, "object3.object4.key6", "value6");
+
+ // And an in-memory stream
+ celix_autofree char* output;
+
+ // When saving the properties to the stream
+ auto status = celix_properties_saveToString(
+ props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithDotsTest) {
+ //Given a properties set with key names with dots
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "a.key.name.with.dots", "value1");
+ celix_properties_set(props, ".keyThatStartsWithDot", "value3");
+ celix_properties_set(props, "keyThatEndsWithDot.", "value5");
+ celix_properties_set(props, "keyThatEndsWithDoubleDots..", "value6");
+ celix_properties_set(props, "key..With..Double..Dots", "value7");
+ celix_properties_set(props, "object.keyThatEndsWithDot.", "value8");
+ celix_properties_set(props, "object.keyThatEndsWithDoubleDots..", "value9");
+ celix_properties_set(props, "object.key..With..Double..Dots", "value10");
+
+
+ //When saving the properties to a string
+ celix_autofree char* output;
+ auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &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":{"dots":"value1"}}}})")) << "JSON: " << output;
+ EXPECT_NE(nullptr, strstr(output, R"("keyThatStartsWithDot":"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"("Dots":"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"("Dots":"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, "Dots");
+ 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
+ celix_autofree char* output1;
+ auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output1);
+
+ //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(output1, R"({"key1":{"key2":"value2"}})")) << "JSON: " << output1;
+
+ //When saving the properties to a string with the error on key collision flag
+ char* output2;
+ status = celix_properties_saveToString(
+ props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output2);
+
+ //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, "Test 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
+ celix_autofree char* output;
+ 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
+ char* output2;
+ status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_STRICT, &output2);
+
+ //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, "Test 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
+ celix_autofree char* output;
+ 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, SaveWithInvalidStreamTest) {
+ celix_autoptr(celix_properties_t) properties = celix_properties_create();
+ celix_properties_set(properties, "key", "value");
+
+ // Saving properties with invalid stream will fail
+ auto status = celix_properties_save(properties, "/non-existing/no/rights/file.json", 0);
+ EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status);
+ EXPECT_EQ(1, celix_err_getErrorCount());
+
+ auto* readStream = fopen("/dev/null", "r");
+ status = celix_properties_saveToStream(properties, readStream, 0);
+ EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status);
+ EXPECT_EQ(2, celix_err_getErrorCount());
+ fclose(readStream);
+
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveCxxPropertiesTest) {
+ // Given a C++ Properties object with 2 keys
+ celix::Properties props{};
+ props.set("key1", "value1");
+ props.set("key2", 42);
+ props.setVector("key3", std::vector<bool>{}); // empty vector
+
+ // When saving the properties to a string
+ std::string result = props.saveToString();
+
+ // Then the result contains the JSON representation snippets of the properties
+ EXPECT_NE(std::string::npos, result.find("\"key1\":\"value1\""));
+ EXPECT_NE(std::string::npos, result.find("\"key2\":42"));
+
+ // When saving the properties to a string using a flat style
+ std::string result2 = props.saveToString(celix::Properties::EncodingFlags::FlatStyle);
+
+ //The result is equals to a default save
+ EXPECT_EQ(result, result2);
+
+ // When saving the properties to a string using an errors on duplicate key flag
+ EXPECT_THROW(props.saveToString(celix::Properties::EncodingFlags::Strict),
+ celix::IllegalArgumentException);
+
+ // When saving the properties to a string using combined flags
+ EXPECT_THROW(props.saveToString(
+ celix::Properties::EncodingFlags::Pretty | celix::Properties::EncodingFlags::ErrorOnEmptyArrays |
+ celix::Properties::EncodingFlags::ErrorOnCollisions |
+ celix::Properties::EncodingFlags::ErrorOnNanInf | celix::Properties::EncodingFlags::NestedStyle),
+ celix::IllegalArgumentException);
+
+ // When saving the properties to an invalid filename location
+ EXPECT_THROW(props.save("/non-existing/no/rights/file.json"),
+ celix::IOException);
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) {
+ //Given an empty JSON object
+ const char* json = "{}";
+
+ //When loading the properties from the stream
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromString2(json, 0, &props);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ //Then the properties object is empty
+ EXPECT_EQ(0, celix_properties_size(props));
+}
+
+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",
+ "longKey":42,
+ "doubleKey":2.0,
+ "boolKey":true,
+ "versionKey":"version<1.2.3.qualifier>"
+ })";
+
+ //And a stream with the JSON object
+ FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+ //When loading the properties from the stream
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromStream(stream, 0, &props);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ //Then the properties object contains the single values
+ EXPECT_EQ(5, celix_properties_size(props));
+ EXPECT_STREQ("strValue", celix_properties_getString(props, "strKey"));
+ EXPECT_EQ(42, celix_properties_getLong(props, "longKey", -1));
+ EXPECT_DOUBLE_EQ(2.0, celix_properties_getDouble(props, "doubleKey", NAN));
+ EXPECT_TRUE(celix_properties_getBool(props, "boolKey", false));
+ auto* v = celix_properties_getVersion(props, "versionKey");
+ ASSERT_NE(nullptr, v);
+ celix_autofree char* vStr = celix_version_toString(v);
+ EXPECT_STREQ("1.2.3.qualifier", vStr);
+}
+
+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"],
+ "intArr":[1,2],
+ "realArr":[1.0,2.0],
+ "boolArr":[true,false],
+ "versionArr":["version<1.2.3.qualifier>","version<4.5.6.qualifier>"],
+ "mixedRealAndIntArr1":[1,2.0,2,3.0],
+ "mixedRealAndIntArr2":[1.0,2,2.0,3]
+ })";
+
+ //And a stream with the JSON object
+ FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+ //When loading the properties from the stream
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromStream(stream, 0, &props);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ //Then the properties object contains the array values
+ EXPECT_EQ(7, celix_properties_size(props));
+
+ //And the string array is correctly loaded
+ auto* strArr = celix_properties_getArrayList(props, "strArr");
+ ASSERT_NE(nullptr, strArr);
+ EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING, celix_arrayList_getElementType(strArr));
+ EXPECT_EQ(2, celix_arrayList_size(strArr));
+ EXPECT_STREQ("value1", celix_arrayList_getString(strArr, 0));
+ EXPECT_STREQ("value2", celix_arrayList_getString(strArr, 1));
+
+ //And the long array is correctly loaded
+ auto* intArr = celix_properties_getArrayList(props, "intArr");
+ ASSERT_NE(nullptr, intArr);
+ EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG, celix_arrayList_getElementType(intArr));
+ EXPECT_EQ(2, celix_arrayList_size(intArr));
+ EXPECT_EQ(1, celix_arrayList_getLong(intArr, 0));
+ EXPECT_EQ(2, celix_arrayList_getLong(intArr, 1));
+
+ //And the double array is correctly loaded
+ auto* realArr = celix_properties_getArrayList(props, "realArr");
+ ASSERT_NE(nullptr, realArr);
+ EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(realArr));
+ EXPECT_EQ(2, celix_arrayList_size(realArr));
+ EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(realArr, 0));
+ EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(realArr, 1));
+
+ //And the bool array is correctly loaded
+ auto* boolArr = celix_properties_getArrayList(props, "boolArr");
+ ASSERT_NE(nullptr, boolArr);
+ EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL, celix_arrayList_getElementType(boolArr));
+ EXPECT_EQ(2, celix_arrayList_size(boolArr));
+ EXPECT_TRUE(celix_arrayList_getBool(boolArr, 0));
+ EXPECT_FALSE(celix_arrayList_getBool(boolArr, 1));
+
+ //And the version array is correctly loaded
+ auto* versionArr = celix_properties_getArrayList(props, "versionArr");
+ ASSERT_NE(nullptr, versionArr);
+ EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION, celix_arrayList_getElementType(versionArr));
+ EXPECT_EQ(2, celix_arrayList_size(versionArr));
+ auto* v1 = celix_arrayList_getVersion(versionArr, 0);
+ ASSERT_NE(nullptr, v1);
+ celix_autofree char* v1Str = celix_version_toString(v1);
+ EXPECT_STREQ("1.2.3.qualifier", v1Str);
+ auto* v2 = celix_arrayList_getVersion(versionArr, 1);
+ ASSERT_NE(nullptr, v2);
+ celix_autofree char* v2Str = celix_version_toString(v2);
+ EXPECT_STREQ("4.5.6.qualifier", v2Str);
+
+ //And the mixed json real and int arrays are correctly loaded as double arrays
+ auto* mixedRealAndIntArr1 = celix_properties_getArrayList(props, "mixedRealAndIntArr1");
+ ASSERT_NE(nullptr, mixedRealAndIntArr1);
+ EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(mixedRealAndIntArr1));
+ EXPECT_EQ(4, celix_arrayList_size(mixedRealAndIntArr1));
+ EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 0));
+ EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 1));
+ EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 2));
+ EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 3));
+
+ auto* mixedRealAndIntArr2 = celix_properties_getArrayList(props, "mixedRealAndIntArr2");
+ ASSERT_NE(nullptr, mixedRealAndIntArr2);
+ EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(mixedRealAndIntArr2));
+ EXPECT_EQ(4, celix_arrayList_size(mixedRealAndIntArr2));
+ EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 0));
+ EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 1));
+ EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 2));
+ EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 3));
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) {
+ auto invalidInputs = {
+ R"({)", // invalid JSON (caught by jansson)
+ R"([])", // unsupported JSON (top level array not supported)
+ R"(42)", // invalid JSON (caught by jansson)
+ };
+ for (auto& invalidInput: invalidInputs) {
+ //Given an invalid JSON object
+ FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r");
+
+ //When loading the properties from the stream
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromStream(stream, 0, &props);
+
+ //Then loading fails
+ EXPECT_NE(CELIX_SUCCESS, status);
+
+ //And at least one error message is added to celix_err
+ EXPECT_GE(celix_err_getErrorCount(), 1);
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+
+ fclose(stream);
+ }
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithEmptyArrayTest) {
+ //Given a JSON object with an empty array
+ auto* inputJSON = R"({"key1":[]})";
+
+ //When loading the properties from string
+ celix_autoptr(celix_properties_t) props = nullptr;
+ 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
+ celix_properties_t* props2;
+ status = celix_properties_loadFromString2(inputJSON, CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, &props2);
+
+ //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, LoadPropertiesWithNestedObjectsTest) {
+ // Given a complex JSON object
+ const char* jsonInput = R"({
+ "key1":"value1",
+ "key2":"value2",
+ "object1": {
+ "key3":"value3",
+ "key4":true
+ },
+ "object2": {
+ "key5":5.0
+ },
+ "object3":{
+ "object4":{
+ "key6":6
+ }
+ }
+ })";
+
+ // And a stream with the JSON object
+ FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+ // When loading the properties from the stream
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromStream(stream, 0, &props);
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ // Then the properties object contains the nested objects
+ EXPECT_EQ(6, celix_properties_size(props));
+ EXPECT_STREQ("value1", celix_properties_getString(props, "key1"));
+ EXPECT_STREQ("value2", celix_properties_getString(props, "key2"));
+ EXPECT_STREQ("value3", celix_properties_getString(props, "object1.key3"));
+ EXPECT_EQ(true, celix_properties_getBool(props, "object1.key4", false));
+ EXPECT_DOUBLE_EQ(5., celix_properties_getDouble(props, "object2.key5", 0.0));
+ EXPECT_EQ(6, celix_properties_getLong(props, "object3.object4.key6", 0));
+}
+
+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_properties_t* 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);
+
+ // And at least one error message is added to celix_err
+ EXPECT_GE(celix_err_getErrorCount(), 1);
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedDotsTest) {
+ // 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": "value1"
+ },
+ "object2.key2": "value2"
+ },
+ "object1.object2" : "value3",
+ "key3": "value4",
+ "key3": "value5",
+ "object3.key4": "value6",
+ "object3": {
+ "key4": "value7"
+ }
+ })";
+
+ // When loading the properties from a string.
+ celix_autoptr(celix_properties_t) props;
+ auto status = celix_properties_loadFromString2(jsonInput, 0, &props);
+
+ // Then loading succeeds
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ // 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"));
+
+ // When decoding the properties from a string using a flag that allows duplicates
+ celix_properties_t* 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);
+
+ // And at least one error message is added to celix_err
+ EXPECT_GE(celix_err_getErrorCount(), 1);
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+
+ // When decoding the properties from a string using a flag that allows collisions
+ celix_properties_t* props3;
+ 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, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagTest) {
+ auto invalidInputs = {
+ R"({"mixedArr":["string", true]})", // Mixed array gives error on strict
+ R"({"mixedVersionAndStringArr":["version<1.2.3.qualifier>","2.3.4"]})", // Mixed array gives error on strict
+ R"({"key1":null})", // Null value gives error on strict
+ R"({"":"value"})", // "" key gives error on strict
+ R"({"emptyArr":[]})", // Empty array gives error on strict
+ R"({"key1":"val1", "key1":"val2"})"// Duplicate key gives error on strict
+ };
+
+ for (auto& invalidInput: invalidInputs) {
+ //Given an invalid JSON object
+ FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r");
+
+ //When loading the properties from the stream with an empty flags
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromStream(stream, 0, &props);
+
+ //Then decoding succeeds, because strict is disabled
+ ASSERT_EQ(CELIX_SUCCESS, status);
+ EXPECT_GE(celix_err_getErrorCount(), 0);
+
+ //But the properties size is 0 or 1, because the all invalid inputs are ignored, except the duplicate key
+ auto size = celix_properties_size(props);
+ EXPECT_TRUE(size == 0 || size == 1);
+
+ fclose(stream);
+ }
+
+ for (auto& invalidInput: invalidInputs) {
+ //Given an invalid JSON object
+ FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r");
+
+ //When loading the properties from the stream with a strict flag
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props);
+
+ //Then decoding fails
+ 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, "Test Error: ", "\n");
+
+ fclose(stream);
+ }
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithUnsupportedArrayTypesTest) {
+ auto invalidArrays = {
+ R"({"objArray":[{"obj1": true}, {"obj2": true}]})", // Array with objects not supported.
+ R"({"arrayArray":[[1,2], [2,4]]})", // Array with array not supported.
+ R"({"nullArr":[null,null]})" // Array with null values gives error on strict
+ };
+
+ // Decode with no strict flag, will ignore the unsupported arrays
+ for (auto& invalidArray : invalidArrays) {
+ // When loading the properties from the string
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromString2(invalidArray, 0, &props);
+
+ // Then decoding succeeds, because strict is disabled
+ ASSERT_EQ(CELIX_SUCCESS, status);
+ EXPECT_GE(celix_err_getErrorCount(), 0);
+
+ // But the properties size is 0, because the all invalid inputs are ignored
+ EXPECT_EQ(0, celix_properties_size(props));
+ }
+
+ // Decode with strict flag, will fail on unsupported arrays
+ for (auto& invalidArray : invalidArrays) {
+ // When loading the properties from the string
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromString2(invalidArray, CELIX_PROPERTIES_DECODE_STRICT, &props);
+
+ // Then decoding fails
+ 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_resetErrors();
+
+ // When loading the properties from the CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS flag
+ celix_properties_t* props2;
+ status = celix_properties_loadFromString2(
+ invalidArray, CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS, &props2);
+
+ // Then decoding fails
+ 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_resetErrors();
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+ }
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDotsInTheKeysTest) {
+ // Given a complex JSON object
+ const char* jsonInput = R"({
+ ".": "value1",
+ "keyThatEndsWithDots.": "value2",
+ "key..With..Double..Dots": "value3",
+ "object": {
+ ".": "value4",
+ "keyThatEndsWithDot.": "value5",
+ "key..With..Double..Dot": "value6"
+ }
+ })";
+
+ // And a stream with the JSON object
+ FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+ // When loading the properties from the stream
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromStream(stream, 0, &props);
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ // Then the properties object contains the nested objects
+ EXPECT_EQ(6, celix_properties_size(props));
+ EXPECT_STREQ("value1", celix_properties_getString(props, "."));
+ EXPECT_STREQ("value2", celix_properties_getString(props, "keyThatEndsWithDots."));
+ EXPECT_STREQ("value3", celix_properties_getString(props, "key..With..Double..Dots"));
+ EXPECT_STREQ("value4", celix_properties_getString(props, "object.."));
+ EXPECT_STREQ("value5", celix_properties_getString(props, "object.keyThatEndsWithDot."));
+ EXPECT_STREQ("value6", celix_properties_getString(props, "object.key..With..Double..Dot"));
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidVersionsTest) {
+ // Given a JSON object with an invalid version string (<, > not allowed in qualifier)
+ const auto* jsonInput = R"( {"key":"version<1.2.3.<qualifier>>"} )";
+
+ // When loading the properties from the stream
+ celix_autoptr(celix_properties_t) props = nullptr;
+ auto status = celix_properties_loadFromString2(jsonInput, 0, &props);
+
+ // Then loading fails
+ 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, "Test Error: ", "\n");
+
+ // Given a JSON object with an invalid version strings, that are not recognized as versions
+ jsonInput =
+ R"( {"key1":"version<1.2.3", "key2":"version1.2.3>", "key3":"ver<1.2.3>}", "key4":"celix_version<1.2.3>"} )";
+
+ // When loading the properties from the stream
+ status = celix_properties_loadFromString2(jsonInput, 0, &props);
+
+ // Then loading succeeds
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ // But the values are not recognized as versions
+ EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key1"));
+ EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key2"));
+ EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key3"));
+ EXPECT_NE(CELIX_PROPERTIES_VALUE_TYPE_VERSION, celix_properties_getType(props, "key4"));
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadWithInvalidStreamTest) {
+ celix_properties_t* dummyProps = nullptr;
+
+ // Loading properties with invalid stream will fail
+ auto status = celix_properties_load2("non_existing_file.json", 0, &dummyProps);
+ EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, status);
+ EXPECT_EQ(1, celix_err_getErrorCount());
+
+ char* buf = nullptr;
+ size_t size = 0;
+ FILE* stream = open_memstream(&buf, &size); // empty stream
+ status = celix_properties_loadFromStream(stream, 0, &dummyProps);
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+ EXPECT_EQ(2, celix_err_getErrorCount());
+
+ fclose(stream);
+ free(buf);
+ celix_err_printErrors(stderr, "Test Error: ", "\n");
+}
+
+TEST_F(PropertiesSerializationTestSuite, LoadCxxPropertiesTest) {
+ // Given a JSON object
+ auto jsonInput = R"({"key1":"value1","key2":42,"key2":43})"; // note duplicate key3
+
+ // When loading the properties from the JSON object
+ auto props = celix::Properties::loadFromString(jsonInput);
+
+ // Then the properties object contains the values
+ EXPECT_EQ(2, props.size());
+ EXPECT_STREQ("value1", props.get("key1").c_str());
+ EXPECT_EQ(43, props.getAsLong("key2", -1));
+
+ // When loading the properties from the JSON object with a strict flag
+ EXPECT_THROW(celix::Properties::loadFromString(jsonInput, celix::Properties::DecodeFlags::Strict),
+ celix::IllegalArgumentException);
+
+ // When loading the properties from the JSON object with a flag combined
+ EXPECT_THROW(
+ celix::Properties::loadFromString(
+ jsonInput,
+ celix::Properties::DecodeFlags::ErrorOnCollisions | celix::Properties::DecodeFlags::ErrorOnDuplicates |
+ celix::Properties::DecodeFlags::ErrorOnEmptyArrays | celix::Properties::DecodeFlags::ErrorOnEmptyKeys |
+ celix::Properties::DecodeFlags::ErrorOnUnsupportedArrays |
+ celix::Properties::DecodeFlags::ErrorOnNullValues | celix::Properties::DecodeFlags::ErrorOnNullValues),
+ celix::IllegalArgumentException);
+
+ EXPECT_THROW(celix::Properties::load2("non_existing_file.json"), celix::IOException);
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveAndLoadFlatProperties) {
+ // Given a properties object with all possible types (but no empty arrays)
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+ celix_properties_set(props, "single/strKey", "strValue");
+ celix_properties_setLong(props, "single/longKey", 42);
+ celix_properties_setDouble(props, "single/doubleKey", 2.0);
+ celix_properties_setBool(props, "single/boolKey", true);
+ celix_properties_assignVersion(props, "single/versionKey", celix_version_create(1, 2, 3, "qualifier"));
+
+ celix_array_list_t* strArr = celix_arrayList_createStringArray();
+ celix_arrayList_addString(strArr, "value1");
+ celix_arrayList_addString(strArr, "value2");
+ celix_properties_assignArrayList(props, "array/stringArr", strArr);
+
+ celix_array_list_t* longArr = celix_arrayList_createLongArray();
+ celix_arrayList_addLong(longArr, 1);
+ celix_arrayList_addLong(longArr, 2);
+ celix_properties_assignArrayList(props, "array/longArr", longArr);
+
+ celix_array_list_t* doubleArr = celix_arrayList_createDoubleArray();
+ celix_arrayList_addDouble(doubleArr, 1.0);
+ celix_arrayList_addDouble(doubleArr, 2.0);
+ celix_properties_assignArrayList(props, "array/doubleArr", doubleArr);
+
+ celix_array_list_t* boolArr = celix_arrayList_createBoolArray();
+ celix_arrayList_addBool(boolArr, true);
+ celix_arrayList_addBool(boolArr, false);
+ celix_properties_assignArrayList(props, "array/boolArr", boolArr);
+
+ celix_array_list_t* versionArr = celix_arrayList_createVersionArray();
+ celix_arrayList_assignVersion(versionArr, celix_version_create(1, 2, 3, "qualifier"));
+ celix_arrayList_assignVersion(versionArr, celix_version_create(4, 5, 6, "qualifier"));
+ celix_properties_assignArrayList(props, "array/versionArr", versionArr);
+
+ // When saving the properties to a properties_test.json file
+ const char* filename = "properties_test.json";
+ auto status = celix_properties_save(props, filename, CELIX_PROPERTIES_ENCODE_PRETTY);
+
+ // Then saving succeeds
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ // When loading the properties from the properties_test.json file
+ celix_autoptr(celix_properties_t) loadedProps = nullptr;
+ status = celix_properties_load2(filename, 0, &loadedProps);
+
+ // Then loading succeeds
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ // And the loaded properties are equal to the original properties
+ EXPECT_TRUE(celix_properties_equals(props, loadedProps));
+}
+
+TEST_F(PropertiesSerializationTestSuite, SaveAndLoadCxxProperties) {
+ //Given a filename
+ std::string filename = "properties_test.json";
+
+ //And a Properties object with 1 key
+ celix::Properties props{};
+ props.set("key1", "value1");
+
+ //When saving the properties to the filename
+ props.save(filename);
+
+ //And reloading the properties from the filename
+ auto props2 = celix::Properties::load2(filename);
+
+ //Then the reloaded properties are equal to the original properties
+ EXPECT_TRUE(props == props2);
+}
+
+TEST_F(PropertiesSerializationTestSuite, JsonErrorToCelixStatusTest) {
+ EXPECT_EQ(CELIX_ILLEGAL_STATE, celix_properties_jsonErrorToStatus(json_error_unknown));
+
+ EXPECT_EQ(ENOMEM, celix_properties_jsonErrorToStatus(json_error_out_of_memory));
+ EXPECT_EQ(ENOMEM, celix_properties_jsonErrorToStatus(json_error_stack_overflow));
+
+ EXPECT_EQ(CELIX_FILE_IO_EXCEPTION, celix_properties_jsonErrorToStatus(json_error_cannot_open_file));
+
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_argument));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_argument));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_premature_end_of_input));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_end_of_input_expected));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_syntax));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_invalid_format));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_wrong_type));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_character));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_value));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_null_byte_in_key));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_duplicate_key));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_numeric_overflow));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_item_not_found));
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_jsonErrorToStatus(json_error_index_out_of_range));
+}
+
+TEST_F(PropertiesSerializationTestSuite, KeyCollision) {
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ // pick keys such that key1 appears before key2 when iterating over the properties
+ celix_properties_set(props, "a.b.haha.arbifdadfsfa", "value1");
+ celix_properties_set(props, "a.b.haha", "value2");
+
+ celix_autofree char* output = nullptr;
+ auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS,
+ &output);
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+
+ celix_autoptr(celix_properties_t) props2 = celix_properties_create();
+ // pick keys such that key1 appears before key2 when iterating over the properties
+ celix_properties_set(props2, "a.b.c", "value1");
+ celix_properties_set(props2, "a.b.c.d", "value2");
+ status = celix_properties_saveToString(props2, CELIX_PROPERTIES_ENCODE_NESTED_STYLE | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS,
+ &output);
+ EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
+ status = celix_properties_saveToString(props2, CELIX_PROPERTIES_ENCODE_NESTED_STYLE, &output);
+ // "a.b.c.d" is silently discarded
+ EXPECT_STREQ(R"({"a":{"b":{"c":"value1"}}})", output);
+ std::cout << output << std::endl;
+ EXPECT_EQ(CELIX_SUCCESS, status);
+}
diff --git a/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc b/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc
index 162a86f..e7916ea 100644
--- a/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesErrorInjectionTestSuite.cc
@@ -46,6 +46,7 @@
celix_ei_expect_open_memstream(nullptr, 0, nullptr);
celix_ei_expect_asprintf(nullptr, 0, -1);
celix_ei_expect_malloc(nullptr, 0, nullptr);
+ celix_ei_expect_calloc(nullptr, 0, nullptr);
celix_ei_expect_celix_stringHashMap_createWithOptions(nullptr, 0, nullptr);
celix_ei_expect_celix_arrayList_copy(nullptr, 0, nullptr);
celix_ei_expect_celix_utils_strdup(nullptr, 0, nullptr);
@@ -453,11 +454,12 @@
celix_err_resetErrors();
celix_autoptr(celix_version_t) version2 = celix_version_create(1, 2, 3, "aaaaaaaaaaaaaaaaaaaaaaaaaa");
- celix_ei_expect_asprintf((void*) celix_version_toString, 0, -1);
+ celix_ei_expect_calloc((void*) celix_version_create, 0, nullptr);
status = celix_properties_setVersion(props, "key", version2);
ASSERT_EQ(status, CELIX_ENOMEM);
- ASSERT_STREQ("Cannot fill property entry", celix_err_popLastError());
- ASSERT_STREQ("Failed to allocate memory for celix_version_toString", celix_err_popLastError());
+ EXPECT_EQ(2, celix_err_getErrorCount());
+ ASSERT_STREQ("Failed to copy version", celix_err_popLastError());
+ ASSERT_STREQ("Failed to allocate memory for celix_version_create", celix_err_popLastError());
celix_err_resetErrors();
fillOptimizationCache(props);
diff --git a/libs/utils/gtest/src/PropertiesTestSuite.cc b/libs/utils/gtest/src/PropertiesTestSuite.cc
index 90636b7..f44e0b0 100644
--- a/libs/utils/gtest/src/PropertiesTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesTestSuite.cc
@@ -24,6 +24,7 @@
#include "celix_err.h"
#include "celix_properties.h"
#include "celix_properties_internal.h"
+#include "celix_stdlib_cleanup.h"
#include "celix_utils.h"
using ::testing::MatchesRegex;
@@ -1005,3 +1006,10 @@
//And when an NULL key is used, a ILLEGAL_ARGUMENT error is returned
EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_setArrayList(props, nullptr, list));
}
+
+TEST_F(PropertiesTestSuite, EmptyStringKeyTest) {
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "", "value"); // "" is a valid key (nullptr is not)
+ EXPECT_EQ(1, celix_properties_size(props));
+ EXPECT_STREQ("value", celix_properties_getString(props, ""));
+}
diff --git a/libs/utils/include/celix/Exception.h b/libs/utils/include/celix/Exception.h
deleted file mode 100644
index 481d6f8..0000000
--- a/libs/utils/include/celix/Exception.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-#pragma once
-
-#include <stdexcept>
-
-namespace celix {
-
- /**
- * @brief Celix runtime Exception
- */
- class Exception : public std::runtime_error {
- public:
- using std::runtime_error::runtime_error;
- };
-
-}
diff --git a/libs/utils/include/celix/Exceptions.h b/libs/utils/include/celix/Exceptions.h
new file mode 100644
index 0000000..d6d24a1
--- /dev/null
+++ b/libs/utils/include/celix/Exceptions.h
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+#pragma once
+
+#include "celix_errno.h"
+
+#include <stdexcept>
+#include <cassert>
+
+namespace celix {
+
+ /**
+ * @brief Celix Generic Exception
+ */
+ class Exception : public std::runtime_error {
+ public:
+ using std::runtime_error::runtime_error;
+ };
+
+ /**
+ * @brief Celix Illegal Argument Exception
+ */
+ class IllegalArgumentException final: public Exception {
+ public:
+ using Exception::Exception;
+ };
+
+ /**
+ * @brief Celix IO Exception
+ */
+ class IOException final: public Exception {
+ public:
+ using Exception::Exception;
+ };
+
+ namespace impl {
+ /**
+ * @brief Utils function to throw a celix::Exception using the given celix_status_t and message.
+ */
+ void inline throwException(celix_status_t status, const std::string& message) {
+ assert(status != CELIX_SUCCESS);
+ const auto* statusMsg = celix_strerror(status);
+ auto msg = std::string{message} + " (" + statusMsg + ")";
+ switch (status) {
+ case CELIX_ILLEGAL_ARGUMENT:
+ throw celix::IllegalArgumentException{msg};
+ case CELIX_FILE_IO_EXCEPTION:
+ throw celix::IOException{msg};
+ default:
+ throw celix::Exception{msg};
+ }
+ }
+ }
+
+}
diff --git a/libs/utils/include/celix/Filter.h b/libs/utils/include/celix/Filter.h
index aecda6d..7a565a9 100644
--- a/libs/utils/include/celix/Filter.h
+++ b/libs/utils/include/celix/Filter.h
@@ -25,7 +25,7 @@
#include "celix_filter.h"
#include "celix_err.h"
-#include "celix/Exception.h"
+#include "celix/Exceptions.h"
#include "celix/Properties.h"
namespace celix {
diff --git a/libs/utils/include/celix/IOException.h b/libs/utils/include/celix/IOException.h
deleted file mode 100644
index 1715801..0000000
--- a/libs/utils/include/celix/IOException.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-#pragma once
-
-#include <exception>
-#if __cplusplus >= 201703L //C++17 or higher
-#include <string_view>
-#endif
-
-namespace celix {
-
- /**
- * @brief Celix runtime IO Exception
- */
- class IOException : public std::exception {
- public:
-#if __cplusplus >= 201703L //C++17 or higher
- explicit IOException(std::string_view msg) : w{msg} {}
-#else
- explicit IOException(std::string msg) : w{std::move(msg)} {}
-#endif
-
- IOException(const IOException&) = default;
- IOException(IOException&&) = default;
- IOException& operator=(const IOException&) = default;
- IOException& operator=(IOException&&) = default;
-
- [[nodiscard]] const char* what() const noexcept override {
- return w.c_str();
- }
- private:
- std::string w;
- };
-
-}
diff --git a/libs/utils/include/celix/Properties.h b/libs/utils/include/celix/Properties.h
index 3b784c5..ee71925 100644
--- a/libs/utils/include/celix/Properties.h
+++ b/libs/utils/include/celix/Properties.h
@@ -30,7 +30,7 @@
#include "celix_properties.h"
#include "celix_utils.h"
#include "celix/Version.h"
-#include "celix/IOException.h"
+#include "celix/Exceptions.h"
namespace celix {
@@ -948,14 +948,168 @@
}
/**
+ * @brief Enum class for encoding flags used in Celix properties JSON encoding.
+ *
+ * The flags are used to control the encoding process and to specify the output format.
+ *
+ * @enum EncodingFlags
+ */
+ enum class EncodingFlags : int {
+ None = 0, /**< No special encoding flags. */
+ Pretty =
+ CELIX_PROPERTIES_ENCODE_PRETTY, /**< Encode in a pretty format, with indentation and line breaks. */
+ FlatStyle =
+ CELIX_PROPERTIES_ENCODE_FLAT_STYLE, /**< Encode in a flat style, with all keys at the top level. */
+ NestedStyle = CELIX_PROPERTIES_ENCODE_NESTED_STYLE, /**< Encode in a nested style, with nested objects for
+ each key based on a `/` separator. */
+ ErrorOnCollisions = CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, /**< If set, encoding will fail if there
+ are collisions between keys. */
+ ErrorOnEmptyArrays = CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, /**< If set, encoding will fail if there
+ are empty arrays. */
+ ErrorOnNanInf = CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF, /**< If set, encoding will fail if there are NaN
+ or Inf values. */
+ Strict = CELIX_PROPERTIES_ENCODE_STRICT, /**< If set, encoding will fail if there are any errors. */
+ };
+
+ /**
+ * @brief Save (encode) this properties object as a JSON representation to a file.
+ *
+ * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream
+ *
+ * For a overview of the possible encode flags, see the EncodingFlags flags documentation.
+ * The default encoding style is a compact and flat JSON representation.
+ *
+ * @param[in] filename The file to write the JSON representation of the properties object to.
+ * @param[in] encodingFlags The flags to use when encoding the input string.
+ * @throws celix::IOException If an error occurs while writing to the file.
+ * @throws celix::IllegalArgumentException If the provided properties cannot be encoded to JSON.
+ * @throws std::bad_alloc If there was not enough memory to save the properties.
+ */
+ void save(const std::string& filename, EncodingFlags encodingFlags = EncodingFlags::None) const {
+ auto status = celix_properties_save(cProps.get(), filename.c_str(), static_cast<int>(encodingFlags));
+ if (status == ENOMEM) {
+ throw std::bad_alloc();
+ } else if (status != CELIX_SUCCESS) {
+ celix::impl::throwException(status, std::string{"Cannot save celix::Properties to "} + filename);
+ }
+ }
+
+ /**
+ * @brief Save (encode) this properties object as a JSON representation to a string.
+ *
+ * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream
+ *
+ * For a overview of the possible encode flags, see the EncodingFlags flags documentation.
+ * The default encoding style is a compact and flat JSON representation.
+ *
+ * @param[in] encodeFlags The flags to use when encoding the input string.
+ * @throws celix::IllegalArgumentException If the provided properties cannot be encoded to JSON.
+ * @throws std::bad_alloc If there was not enough memory to save the properties.
+ */
+ std::string saveToString(EncodingFlags encodeFlags = EncodingFlags::None) const {
+ char* str = nullptr;
+ auto status = celix_properties_saveToString(cProps.get(), static_cast<int>(encodeFlags), &str);
+ if (status == ENOMEM) {
+ throw std::bad_alloc();
+ } else if (status != CELIX_SUCCESS) {
+ celix::impl::throwException(status, "Cannot save celix::Properties to string");
+ }
+ std::string result{str};
+ free(str);
+ return result;
+ }
+
+ /**
+ * @brief Enum class for decoding flags used in Celix properties JSON decoding.
+ *
+ * The flags are used to control the decoding process and to specify the output format.
+ *
+ * @enum DecodeFlags
+ */
+ enum class DecodeFlags : int {
+ None = 0, /**< No special decoding flags. */
+ ErrorOnDuplicates = CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, /**< If set, decoding will fail if there
+ are duplicate keys. */
+ ErrorOnCollisions = CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS, /**< If set, decoding will fail if there
+ are collisions between keys. */
+ ErrorOnNullValues = CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES, /**< If set, decoding will fail if there
+ are null values. */
+ ErrorOnEmptyArrays = CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, /**< If set, decoding will fail if there
+ are empty arrays. */
+ ErrorOnEmptyKeys = CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS, /**< If set, decoding will fail if there are
+ empty ("") keys. */
+ ErrorOnUnsupportedArrays =
+ CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS, /**< If set, decoding will fail if there
+ are unsupported array types or mixed array types. */
+ Strict = CELIX_PROPERTIES_DECODE_STRICT /**< If set, decoding will fail if there are any errors. */
+ };
+
+ /**
* @brief Loads properties from the file at the given path.
* @param[in] path The path to the file containing the properties.
* @return A new Properties object containing the properties from the file.
* @throws celix::IOException If the file cannot be opened or read.
+ * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object.
+ * @throws std::bad_alloc If there was not enough memory to load the properties.
*/
- static celix::Properties load(const std::string& path) { return loadFrom(path.data()); }
+ static Properties load(const std::string& path) { return loadFrom(path.data()); }
- private:
+ /**
+ * @brief Load a Properties object from a file.
+ *
+ * @warning The name is temporary and will be renamed to celix::Properties::load in the future (when
+ * the current celix::Properties::load is removed).
+ *
+ * The content of the filename file is expected to be in the format of a JSON object.
+ * For what can and cannot be parsed, see celix_properties_loadFromStream documentation.
+ *
+ * For a overview of the possible decode flags, see the DecodingFlags flags documentation.
+ *
+ * @param[in] filename The file to load the properties from.
+ * @param[in] decodeFlags The flags to use when decoding the input string.
+ * @return A new Properties object containing the properties from the file.
+ * @throws celix::IOException If the file cannot be opened or read.
+ * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object.
+ * @throws std::bad_alloc If there was not enough memory to load the properties.
+ */
+ static Properties load2(const std::string& filename, DecodeFlags decodeFlags = DecodeFlags::None) {
+ celix_properties_t* props;
+ auto status = celix_properties_load2(filename.c_str(), static_cast<int>(decodeFlags), &props);
+ if (status == ENOMEM) {
+ throw std::bad_alloc();
+ } else if (status != CELIX_SUCCESS) {
+ celix::impl::throwException(status, "Cannot load celix::Properties from " + filename);
+ }
+ return celix::Properties::own(props);
+ }
+
+ /**
+ * @brief Load a Properties object from a string.
+ *
+ *
+ * The input string is expected to be in the format of a JSON object.
+ * For what can and cannot be parsed, see celix_properties_loadFromStream documentation.
+ *
+ * For a overview of the possible decode flags, see the DecodingFlags flags documentation.
+ *
+ * @param[in] input The input string to parse.
+ * @param[in] decodeFlags The flags to use when decoding the input string.
+ * @return A new Properties object containing the properties from the file.
+ * @throws celix::IllegalArgumentException if the provided input cannot be decoded to a properties object.
+ * @throws std::bad_alloc If there was not enough memory to load the properties.
+ */
+ static Properties loadFromString(const std::string& input, DecodeFlags decodeFlags = DecodeFlags::None) {
+ celix_properties_t* props;
+ auto status = celix_properties_loadFromString2(input.c_str(), static_cast<int>(decodeFlags), &props);
+ if (status == ENOMEM) {
+ throw std::bad_alloc();
+ } else if (status != CELIX_SUCCESS) {
+ celix::impl::throwException(status, "Cannot load celix::Properties from string");
+ }
+ return celix::Properties::own(props);
+ }
+
+ private:
Properties(celix_properties_t* props, bool takeOwnership) :
cProps{props, [takeOwnership](celix_properties_t* p){ if (takeOwnership) { celix_properties_destroy(p); }}} {}
@@ -1045,8 +1199,36 @@
};
}
+/**
+ * @brief Stream operator to print the properties value reference to a stream.
+ * @param[in] os The stream to print the properties to.
+ * @param[in] ref The properties value reference to print.
+ * @return The os stream.
+ */
inline std::ostream& operator<<(std::ostream& os, const ::celix::Properties::ValueRef& ref)
{
os << std::string{ref.getValue()};
return os;
}
+
+/**
+ * @brief Bitwise OR operator for EncodingFlags.
+ * @param[in] a encoding flags
+ * @param[in] b encoding flags
+ * @return The bitwise OR of the two encoding flags.
+ */
+inline ::celix::Properties::EncodingFlags operator|(::celix::Properties::EncodingFlags a,
+ ::celix::Properties::EncodingFlags b) {
+ return static_cast<::celix::Properties::EncodingFlags>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+/**
+ * @brief Bitwise OR operator for DecodeFlags.
+ * @param[in] a decoding flags
+ * @param[in] b decoding flags
+ * @return The bitwise OR of the two decoding flags.
+ */
+inline ::celix::Properties::DecodeFlags operator|(::celix::Properties::DecodeFlags a,
+ ::celix::Properties::DecodeFlags b) {
+ return static_cast<::celix::Properties::DecodeFlags>(static_cast<int>(a) | static_cast<int>(b));
+}
diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h
index 7c7a3e9..f49d462 100644
--- a/libs/utils/include/celix_properties.h
+++ b/libs/utils/include/celix_properties.h
@@ -57,7 +57,7 @@
*/
typedef enum celix_properties_value_type {
CELIX_PROPERTIES_VALUE_TYPE_UNSET = 0, /**< Property value is not set. */
- CELIX_PROPERTIES_VALUE_TYPE_STRING = 1, /**< Property value is a string. */
+ CELIX_PROPERTIES_VALUE_TYPE_STRING = 1, /**< Property value is a UTF-8 encoded string. */
CELIX_PROPERTIES_VALUE_TYPE_LONG = 2, /**< Property value is a long integer. */
CELIX_PROPERTIES_VALUE_TYPE_DOUBLE = 3, /**< Property value is a double. */
CELIX_PROPERTIES_VALUE_TYPE_BOOL = 4, /**< Property value is a boolean. */
@@ -936,6 +936,357 @@
!celix_propertiesIterator_isEnd(&(iterName)); \
celix_propertiesIterator_next(&(iterName)))
+/**
+ * @brief Flag to indicate that the encoded output should be pretty; e.g. encoded with additional whitespaces,
+ * newlines and indentation.
+ *
+ * If this flag is not set, the encoded output will compact; e.g. without additional whitespaces, newlines and
+ * indentation.
+ */
+#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01
+
+/**
+ * @brief Flag to indicate that the encoded output should be flat; e.g. all properties entries are written as top level
+ * field entries.
+ *
+ * E.g:
+ * @code{.c}
+ * celix_properties_t* properties = celix_properties_create();
+ * celix_properties_setString(properties, "key/with/slash", "value1");
+ * celix_properties_setString(properties, "key", "value2");
+ * char* json;
+ * celix_properties_saveToString(properties, CELIX_PROPERTIES_ENCODE_FLAT, &json);
+ * // json will be: {"key/with/slash": "value1", "key": "value2"}
+ * @endcode
+ *
+ * Note that encoding with a flat encoding style, all properties keys are unique JSON keys and can be written.
+ *
+ * If no encoding style flag is set, the encoded output will use the default encoding style.
+ */
+#define CELIX_PROPERTIES_ENCODE_FLAT_STYLE 0x02
+
+/**
+ * @brief Flag to indicate that the encoded output should be nested; e.g. properties entries are split on '/' and nested
+ * in JSON objects.
+ *
+ * E.g:
+ * @code{.c}
+ * celix_properties_t* properties = celix_properties_create();
+ * celix_properties_setString(properties, "key/with/slash", "value1");
+ * celix_properties_setString(properties, "key", "value2");
+ * char* json;
+ * celix_properties_saveToString(properties, CELIX_PROPERTIES_ENCODE_NESTED, &json);
+ * // json will be: {"key":{"with":{"slash": "value1"}}}
+ * // or
+ * // json will be: {"key": "value2"}
+ * @endcode
+ *
+ * Note that encoding with a nested encoding style, it properties key can collide resulting in missing properties
+ * entries or (if CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS is set) an error.
+ *
+ * If no encoding style flag is set, the encoded output will use the default encoding style.
+ */
+#define CELIX_PROPERTIES_ENCODE_NESTED_STYLE 0x04
+
+/**
+ * @brief Flag to indicate that the encoding should fail if the JSON representation will contain colliding keys.
+ *
+ * Note that colliding keys can only occur when using the nested encoding style.
+ *
+ * E.g. the following will lead to an error:
+ * @code{.c}
+ * celix_properties_t* properties = celix_properties_create();
+ * celix_properties_setString(properties, "key/with/slash", "value1");
+ * celix_properties_setString(properties, "key", "value2"); //collision
+ * char* json;
+ * celix_status_t status = celix_properties_saveToString(properties,
+ * CELIX_PROPERTIES_ENCODE_NESTED | | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &json);
+ * // status will be CELIX_ILLEGAL_ARGUMENT and a error message will be logged to celix_err
+ * @endcode
+ *
+ * If this flag is set, the encoding will fail if the JSON representation will contain colliding keys and if this flag
+ * is not set, the encoding will not fail and the colliding keys will be ignored.
+ */
+#define CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS 0x10
+
+/**
+ * @brief Flag to indicate that the encoding should fail if the JSON representation will contain empty arrays.
+ *
+ * Although empty arrays are valid in JSON, they cannot be decoded to a valid properties array entry and as such
+ * empty arrays properties entries are not encoded.
+ *
+ * If this flag is set, the encoding will fail if the JSON representation will contain empty arrays and if this flag
+ * is not set, the encoding will not fail and the empty arrays will be ignored.
+ */
+#define CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS 0x20
+
+/**
+ * @brief Flag to indicate that the encoding should fail if the JSON representation will contain NaN or Inf values.
+ *
+ * NaN, Inf and -Inf are not valid JSON values and as such properties entries with these values are not encoded.
+ *
+ * If this flag is set, the encoding will fail if the JSON representation will contain NaN or Inf values and if this
+ * flag is not set, the encoding will not fail and the NaN and Inf entries will be ignored.
+ */
+#define CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF 0x40
+
+/**
+ * @brief Flag to indicate that all encode "error on" flags should be set.
+ */
+#define CELIX_PROPERTIES_ENCODE_STRICT \
+ (CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS | CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS | \
+ CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF)
+
+/**
+ * @brief Save (encode) as a JSON representation to a stream.
+ *
+ * The stream is expected to be a valid stream and is not reset or closed by this function.
+ *
+ * Properties are encoded as a JSON object.
+ *
+ * If no encoding style flag is set or when the CELIX_PROPERTIES_ENCODE_FLAT_STYLE flag is set, properties
+ * entries are written as top level field entries.
+ *
+ * If the CELIX_PROPERTIES_ENCODE_NESTED_STYLE flag is set, properties entry keys are split on '/' and nested in
+ * JSON objects. This leads to a more natural JSON representation, but if there are colliding properties keys (e.g.
+ * `{"key": "value1", "key/with/slash": "value2"}`), not all properties entries will be written.
+ *
+ * With all encoding styles, the empty array properties entries are ignored, because they cannot be decoded to a valid
+ * properties array entry.
+ *
+ * Properties type entries are encoded as follows:
+ * - CELIX_PROPERTIES_TYPE_STRING: The value is encoded as a JSON string.
+ * - CELIX_PROPERTIES_TYPE_LONG: The value is encoded as a JSON number.
+ * - CELIX_PROPERTIES_TYPE_DOUBLE: The value is encoded as a JSON number.
+ * - CELIX_PROPERTIES_TYPE_BOOL: The value is encoded as a JSON boolean.
+ * - CELIX_PROPERTIES_TYPE_ARRAY: The value is encoded as a JSON array, with each element encoded according to its type.
+ * - CELIX_PROPERTIES_TYPE_VERSION: The value is encoded as a JSON string with a "version<" prefix and a ">" suffix
+ * (e.g. "version<1.2.3>").
+ *
+ * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation.
+ * The default encoding style is a compact and flat JSON representation.
+ *
+ * @param properties The properties object to encode.
+ * @param stream The stream to write the JSON representation of the properties object to.
+ * @param encodeFlags The flags to use when encoding the input properties.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be
+ * encoded to a JSON representation, ENOMEM if there was not enough memory and CELIX_FILE_IO_EXCEPTION if the stream
+ * could not be written to.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties,
+ FILE* stream,
+ int encodeFlags);
+
+/**
+ * @brief Save (encode) properties as a JSON representation to a file.
+ *
+ * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream
+ *
+ * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation.
+ * The default encoding style is a compact and flat JSON representation.
+ *
+ * @param[in] properties The properties object to encode.
+ * @param[in] filename The file to write the JSON representation of the properties object to.
+ * @param[in] encodeFlags The flags to use when encoding the input properties.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be
+ * encoded to a JSON representation and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file
+ * could not be opened or written to.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_save(const celix_properties_t* properties,
+ const char* filename,
+ int encodeFlags);
+
+/**
+ * @brief Save (encode) properties as a JSON representation to a string.
+ *
+ * For more information how a properties object is encoded to JSON, see the celix_properties_loadFromStream
+ *
+ * For a overview of the possible encode flags, see the CELIX_PROPERTIES_ENCODE_* flags documentation.
+ * The default encoding style is a compact and flat JSON representation.
+ *
+ * @param[in] properties The properties object to encode.
+ * @param[in] encodeFlags The flags to use when encoding the input properties.
+ * @param[out] out The JSON string representation of the properties object. The caller is responsible for freeing the
+ * returned string using free.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided properties cannot be
+ * encoded to a JSON representation and ENOMEM if there was not enough memory.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_properties_t* properties,
+ int encodeFlags,
+ char** out);
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains duplicate JSON keys.
+ *
+ * E.g. `{"key": "value", "key": "value2"}` is a duplicate key.
+ *
+ * If this flag is set, the decoding will fail if the input contains a duplicate key and if this flag is not set, the
+ * decoding will not fail and the last entry will be used.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains entry that collide on property keys.
+ *
+ * E.g. `{"obj/key": "value", "obj": {"key": "value2"}}` is a collision.
+ *
+ * If this flag is set, the decoding will fail if the input contains a collision and if this flag is not set, the
+ * decoding will not fail and the last entry will be used.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS 0x02
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains null values.
+ *
+ * E.g. `{"key": null}` is a null value.
+ *
+ * Note arrays with null values are handled by the CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS flag.
+ *
+ * If this flag is set, the decoding will fail if the input contains a null value and if this flag is not set, the
+ * decoding will not fail and the JSON null entry will be ignored.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x04
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains empty arrays.
+ *
+ *
+ * E.g. `{"key": []}` is an empty array.
+ *
+ * Note that empty arrays are valid in JSON, but not cannot be decoded to a valid properties array entry.
+ *
+ * If this flag is set, the decoding will fail if the input contains an empty array and if this flag is not set, the
+ * decoding will not fail and the JSON empty array entry will be ignored.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x08
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains unsupported arrays.
+ *
+ * Unsupported arrays are arrays that contain JSON objects, multiple arrays, arrays with null values and
+ * mixed arrays.
+ * E.g.
+ * - `{"key": [{"nested": "value"}]}` (array with JSON object)
+ * - `{"key": [[1,2],[3,4]]}` (array with array)
+ * - `{"key": [null,null]}` (array with null values)
+ * - `{"key": ["value", 1]}` (mixed array)
+ *
+ * If this flag is set, the decoding will fail if the input contains an unsupported array and if this flag is not set,
+ * the decoding will not fail and the unsupported array entries will be ignored.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS 0x10
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains empty keys.
+ *
+ * E.g. `{"": "value"}` is an empty key.
+ *
+ * Note that empty keys are valid in JSON and valid in properties, but not always desired.
+ *
+ * If this flag is set, the decoding will fail if the input contains an empty key.
+ */
+#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x20
+
+/**
+ * @brief Flag to indicate that the decoding should fail if the input contains any of the decode error flags.
+ *
+ * This flag is a combination of all decode error flags.
+ */
+#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_UNSUPPORTED_ARRAYS | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS)
+
+/**
+ * @brief Load properties from a stream.
+ *
+ * The stream is expected to be a valid readable stream and is not reset or closed by this function.
+ * The content of the stream is expected to be in the format of a JSON object.
+ *
+ * For decoding a single JSON object is decoded to a properties object.
+ *
+ * The keys of the JSON object are used as
+ * properties keys and the values of the JSON object are used as properties values. If there are nested
+ * JSON objects, the keys are concatenated with a '/' separator (e.g. `{"key": {"nested": "value"}}` will be
+ * decoded to a properties object with a single entry with key `key/nested` and (string) value `value`).
+ *
+ * Because properties keys are created by concatenating the JSON keys, there there could be collisions
+ * (e.g. `{"obj/key": "value", "obj": {"key": "value2"}}`, two entries with the key `obj/key`. In this case
+ * the last decoded JSON entry will be used.
+ *
+ * Properties entry types are determined by the JSON value type:
+ * - JSON string values are decoded as string properties entries.
+ * - JSON number values are decoded as long or double properties entries, depending on the value.
+ * - JSON boolean values are decoded as boolean properties entries.
+ * - jSON string values with a "version<" prefix and a ">" suffix are decoded as version properties entries (e.g.
+ * "version<1.2.3>").
+ * - JSON array values are decoded as array properties entries. The array can contain any of the above types, but mixed
+ * arrays are not supported.
+ * - JSON null values are ignored.
+ *
+ * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation.
+ *
+ * @param[in] stream The input stream to parse.
+ * @param[in] decodeFlags The flags to use when decoding the input string.
+ * @param[out] out The properties object that will be created from the input string. The caller is responsible for
+ * freeing the returned properties object using celix_properties_destroy.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be
+ * decoded to a properties object and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file
+ * could not be read.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream,
+ int decodeFlags,
+ celix_properties_t** out);
+
+/**
+ * @brief Load properties from a file.
+ *
+ * @warning The name is temporary and will be renamed to celix_properties_load in the future (when
+ * the current celix_properties_load is removed).
+ *
+ * The content of the filename file is expected to be in the format of a JSON object.
+ * For what can and cannot be parsed, see celix_properties_loadFromStream documentation.
+ *
+ * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation.
+ *
+ * If an error occurs, the error status is returned and a message is logged to celix_err.
+ *
+ * @param[in] filename The file to load the properties from.
+ * @param[in] decodeFlags The flags to use when decoding the input string.
+ * @param[out] out The properties object that will be created from the input string. The caller is responsible for
+ * freeing the returned properties object using celix_properties_destroy.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be
+ * decoded to a properties object and ENOMEM if there was not enough memory. CELIX_FILE_IO_EXCEPTION if the file
+ * could not be opened.
+ */
+CELIX_UTILS_EXPORT celix_status_t celix_properties_load2(const char* filename,
+ int decodeFlags,
+ celix_properties_t** out);
+
+/**
+ * @brief Load properties from a string.
+ *
+ * @warning The name is temporary and will be renamed to celix_properties_loadFromString in the future (when
+ * the current celix_properties_loadFromString is removed).
+ *
+ * The input string is expected to be in the format of a JSON object.
+ * For what can and cannot be parsed, see celix_properties_loadFromStream documentation.
+ *
+ * For a overview of the possible decode flags, see the CELIX_PROPERTIES_DECODE_* flags documentation.
+ *
+ * If an error occurs, the error status is returned and a message is logged to celix_err.
+ *
+ * @param[in] input The input string to parse.
+ * @param[in] decodeFlags The flags to use when decoding the input string.
+ * @param[out] out The properties object that will be created from the input string. The caller is responsible for
+ * freeing the returned properties object using celix_properties_destroy.
+ * @return CELIX_SUCCESS if the operation was successful, CELIX_ILLEGAL_ARGUMENT if the provided input cannot be
+ * decoded to a properties object and ENOMEM if there was not enough memory.
+ */
+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 7933b4e..19a4fd9 100644
--- a/libs/utils/include/celix_utils.h
+++ b/libs/utils/include/celix_utils.h
@@ -29,6 +29,8 @@
#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,59 @@
*/
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 not 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 or
+ * 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/celix_properties_private.h b/libs/utils/src/celix_properties_private.h
index 209b2ef..350a373 100644
--- a/libs/utils/src/celix_properties_private.h
+++ b/libs/utils/src/celix_properties_private.h
@@ -26,11 +26,13 @@
#define CELIX_CELIX_PROPERTIES_PRIVATE_H
#include "celix_properties.h"
+#include "jansson.h"
#ifdef __cplusplus
extern "C" {
#endif
+
/**
* @brief Alloc new entry for the provided properties. Possible using the properties optimizer cache.
*/
@@ -41,6 +43,11 @@
*/
char* celix_properties_createString(celix_properties_t* properties, const char* str);
+/**
+ * @brief Convert jansson error to celix status.
+ */
+celix_status_t celix_properties_jsonErrorToStatus(enum json_error_code error);
+
#ifdef __cplusplus
}
diff --git a/libs/utils/src/properties.c b/libs/utils/src/properties.c
index e00573a..5c8b657 100644
--- a/libs/utils/src/properties.c
+++ b/libs/utils/src/properties.c
@@ -334,6 +334,8 @@
free(props);
props = NULL;
}
+ } else {
+ celix_err_push("Cannot allocate memory for properties");
}
return props;
}
@@ -451,7 +453,6 @@
celix_autoptr(celix_properties_t) props = celix_properties_create();
if (!props) {
- celix_err_push("Failed to create properties");
return NULL;
}
@@ -634,7 +635,7 @@
return entry;
}
-static const bool celix_properties_isEntryArrayListWithElType(const celix_properties_entry_t* entry,
+static bool celix_properties_isEntryArrayListWithElType(const celix_properties_entry_t* entry,
celix_array_list_element_type_t elType) {
return entry != NULL && entry->valueType == CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST &&
celix_arrayList_getElementType(entry->typed.arrayValue) == elType;
diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c
new file mode 100644
index 0000000..53af5ec
--- /dev/null
+++ b/libs/utils/src/properties_encoding.c
@@ -0,0 +1,644 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "celix_properties.h"
+#include "celix_properties_private.h"
+
+#include "celix_err.h"
+#include "celix_stdlib_cleanup.h"
+#include "celix_utils.h"
+
+#include <assert.h>
+#include <jansson.h>
+#include <math.h>
+#include <string.h>
+
+#define CELIX_PROPERTIES_JSONPATH_SEPARATOR '.'
+
+static celix_status_t
+celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags);
+
+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) {
+ celix_err_push("Failed to create version string.");
+ return ENOMEM;
+ }
+ *out = json_sprintf("version<%s>", versionStr);
+ if (!*out) {
+ celix_err_push("Failed to create json string.");
+ return ENOMEM;
+ }
+ return CELIX_SUCCESS;
+}
+
+static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType,
+ celix_array_list_entry_t entry,
+ int flags,
+ json_t** out) {
+ *out = NULL;
+ switch (elType) {
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING:
+ *out = json_string(entry.stringVal);
+ break;
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG:
+ *out = json_integer(entry.longVal);
+ break;
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE:
+ if (isnan(entry.doubleVal) || isinf(entry.doubleVal)) {
+ if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) {
+ celix_err_push("Invalid NaN or Inf.");
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ return CELIX_SUCCESS; // ignore NaN and Inf
+ }
+ *out = json_real(entry.doubleVal);
+ break;
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL:
+ *out = json_boolean(entry.boolVal);
+ break;
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION:
+ return celix_properties_versionToJson(entry.versionVal, out);
+ default:
+ // LCOV_EXCL_START
+ celix_err_pushf("Invalid array list element type %d.", elType);
+ return CELIX_ILLEGAL_ARGUMENT;
+ // LCOV_EXCL_STOP
+ }
+ if (!*out) {
+ celix_err_push("Failed to create json value.");
+ return ENOMEM;
+ }
+ return CELIX_SUCCESS;
+}
+
+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
+ }
+
+ json_auto_t* array = json_array();
+ if (!array) {
+ celix_err_push("Failed to create json array.");
+ return ENOMEM;
+ }
+
+ int size = celix_arrayList_size(entry->typed.arrayValue);
+ celix_array_list_element_type_t elType = celix_arrayList_getElementType(entry->typed.arrayValue);
+ for (int i = 0; i < size; ++i) {
+ celix_array_list_entry_t arrayEntry = celix_arrayList_getEntry(entry->typed.arrayValue, i);
+ json_t* jsonValue;
+ celix_status_t status = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry, flags, &jsonValue);
+ if (status != CELIX_SUCCESS) {
+ celix_err_pushf("Failed to encode array element(%d) for key %s.", i, key);
+ return status;
+ } else if (!jsonValue) {
+ // ignore unset values
+ } else {
+ int rc = json_array_append_new(array, jsonValue);
+ if (rc != 0) {
+ celix_err_push("Failed to append json string to array.");
+ return ENOMEM;
+ }
+ }
+ }
+
+ if (json_array_size(array) == 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
+ }
+
+ *out = celix_steal_ptr(array);
+ return CELIX_SUCCESS;
+}
+
+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:
+ *out = json_string(entry->value);
+ break;
+ case CELIX_PROPERTIES_VALUE_TYPE_LONG:
+ *out = json_integer(entry->typed.longValue);
+ break;
+ case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE:
+ if (isnan(entry->typed.doubleValue) || isinf(entry->typed.doubleValue)) {
+ if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_NAN_INF) {
+ celix_err_pushf("Invalid NaN or Inf in key '%s'.", key);
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ return CELIX_SUCCESS; // ignore NaN and Inf
+ }
+ *out = json_real(entry->typed.doubleValue);
+ break;
+ case CELIX_PROPERTIES_VALUE_TYPE_BOOL:
+ *out = json_boolean(entry->typed.boolValue);
+ break;
+ 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(key, entry, flags, out);
+ default:
+ // LCOV_EXCL_START
+ celix_err_pushf("Unexpected properties entry type %d.", entry->valueType);
+ return CELIX_ILLEGAL_ARGUMENT;
+ // LCOV_EXCL_STOP
+ }
+
+ if (!*out) {
+ celix_err_pushf("Failed to create json value for key '%s'.", key);
+ return ENOMEM;
+ }
+ return CELIX_SUCCESS;
+}
+
+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);
+ json_decref(value);
+ 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 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);
+ status = CELIX_DO_IF(status, celix_properties_addJsonValueToJson(value, key, root, flags));
+ return status;
+}
+
+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* fieldName = key;
+ const char* slash = strchr(key, CELIX_PROPERTIES_JSONPATH_SEPARATOR);
+ while (slash) {
+ 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 ENOMEM;
+ }
+ json_t* subObj = json_object_get(jsonObj, name);
+ if (subObj && !json_is_object(subObj)) {
+ if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) {
+ celix_err_pushf("Invalid key collision. Key '%s' already exists.", name);
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ return CELIX_SUCCESS;
+ }
+ if (!subObj) {
+ subObj = json_object();
+ if (!subObj) {
+ celix_err_push("Failed to create json object");
+ return ENOMEM;
+ }
+ int rc = json_object_set_new(jsonObj, name, subObj);
+ if (rc != 0) {
+ celix_err_push("Failed to set json object");
+ return ENOMEM;
+ }
+ }
+
+ jsonObj = subObj;
+ fieldName = slash + 1;
+ slash = strchr(fieldName, CELIX_PROPERTIES_JSONPATH_SEPARATOR);
+ }
+
+ json_t* value;
+ celix_status_t status = celix_properties_entryValueToJson(fieldName, entry, flags, &value);
+ if (status != CELIX_SUCCESS) {
+ return status;
+ }
+ return celix_properties_addJsonValueToJson(value, fieldName, jsonObj, flags);
+}
+
+celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) {
+ json_auto_t* root = json_object();
+ if (!root) {
+ celix_err_push("Failed to create json object");
+ return ENOMEM;
+ }
+
+ if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT_STYLE) && !(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED_STYLE)) {
+ //no encoding flags set, default to flat
+ encodeFlags |= CELIX_PROPERTIES_ENCODE_FLAT_STYLE;
+ }
+
+ CELIX_PROPERTIES_ITERATE(properties, iter) {
+ celix_status_t status;
+ if (encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT_STYLE) {
+ status = celix_properties_addPropertiesEntryFlatToJson(&iter.entry, iter.key, root, encodeFlags);
+ } else {
+ assert(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED_STYLE);
+ status = celix_properties_addPropertiesEntryToJson(&iter.entry, iter.key, root, encodeFlags);
+ }
+ if (status != CELIX_SUCCESS) {
+ return status;
+ }
+ }
+
+ size_t jsonFlags = JSON_COMPACT;
+ if (encodeFlags & CELIX_PROPERTIES_ENCODE_PRETTY) {
+ jsonFlags = JSON_INDENT(2);
+ }
+
+ int rc = json_dumpf(root, stream, jsonFlags);
+ if (rc != 0) {
+ celix_err_push("Failed to dump json object to stream.");
+ return CELIX_FILE_IO_EXCEPTION;
+ }
+ 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);
+ int rc = fclose(stream);
+ if (rc != 0) {
+ celix_err_pushf("Failed to close file %s: %s", filename, strerror(errno));
+ return CELIX_FILE_IO_EXCEPTION;
+ }
+ 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 ENOMEM;
+ }
+
+ celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags);
+ (void)fclose(stream);
+ if (!buffer || status != CELIX_SUCCESS) {
+ if (!buffer || status == CELIX_FILE_IO_EXCEPTION) {
+ return ENOMEM; // Using memstream as stream, return ENOMEM instead of CELIX_FILE_IO_EXCEPTION
+ }
+ return status;
+ }
+ *out = celix_steal_ptr(buffer);
+ return CELIX_SUCCESS;
+}
+
+static celix_status_t celix_properties_parseVersion(const char* value, celix_version_t** out) {
+ // precondition: value is a valid version string (8 chars prefix and 1 char suffix)
+ *out = NULL;;
+ char buf[32];
+ char* extracted = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)strlen(value) - 9, value + 8);
+ celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buf, extracted);
+ if (!extracted) {
+ celix_err_push("Failed to create extracted version string.");
+ return ENOMEM;
+ }
+ celix_status_t status = celix_version_parse(extracted, out);
+ if (status != CELIX_SUCCESS) {
+ celix_err_push("Failed to parse version string.");
+ return status;
+ }
+ return CELIX_SUCCESS;
+}
+
+static bool celix_properties_isVersionString(const char* value) {
+ return strncmp(value, "version<", 8) == 0 && value[strlen(value) - 1] == '>';
+}
+
+/**
+ * @brief Determine the array list element type based on the json value.
+ *
+ * If the array is of a mixed type, the element type cannot be determined and a CELIX_ILLEGAL_ARGUMENT is
+ * returned.
+ *
+ * @param[in] value The json value.
+ * @param[out] out The array list element type.
+ * @return CELIX_SUCCESS if the array list element type could be determined or CELIX_ILLEGAL_ARGUMENT if the array
+ * type could not be determined.
+ */
+static celix_status_t celix_properties_determineArrayType(const json_t* jsonArray,
+ celix_array_list_element_type_t* out) {
+ assert(json_array_size(jsonArray) > 0); //precondition: size > 0
+
+ json_t* value;
+ int index;
+ json_type type = JSON_NULL;
+ bool versionType = false;
+ json_array_foreach(jsonArray, index, value) {
+ if (index == 0) {
+ type = json_typeof(value);
+ if (type == JSON_STRING && celix_properties_isVersionString(json_string_value(value))) {
+ versionType = true;
+ }
+ } else if ((type == JSON_TRUE || type == JSON_FALSE) && json_is_boolean(value)) {
+ // bool, ok.
+ continue;
+ } else if (type == JSON_INTEGER && json_typeof(value) == JSON_REAL) {
+ // mixed integer and real, ok but promote to real
+ type = JSON_REAL;
+ continue;
+ } else if (type == JSON_REAL && json_typeof(value) == JSON_INTEGER) {
+ // mixed real and integer, ok
+ continue;
+ } else if (type != json_typeof(value)) {
+ return CELIX_ILLEGAL_ARGUMENT;
+ } else if (versionType) {
+ if (json_typeof(value) != JSON_STRING || !celix_properties_isVersionString(json_string_value(value))) {
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ }
+ }
+
+ switch (type) {
+ case JSON_STRING:
+ *out = versionType ? CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION : CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING;
+ break;
+ case JSON_INTEGER:
+ *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG;
+ break;
+ case JSON_REAL:
+ *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE;
+ break;
+ case JSON_TRUE:
+ case JSON_FALSE:
+ *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL;
+ break;
+ default:
+ //JSON_NULL, JSON_OBJECT and JSON_ARRAY
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+
+ return CELIX_SUCCESS;
+}
+
+static celix_status_t
+celix_properties_decodeArray(celix_properties_t* props, const char* key, const json_t* jsonArray, int flags) {
+ celix_array_list_element_type_t elType;
+ celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType);
+ if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_UNSUPPORTED_ARRAYS)) {
+ celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY);
+ celix_err_pushf("Invalid mixed, null, object or multidimensional array for key '%s': %s.", key, arrStr);
+ return status;
+ } else if (status != CELIX_SUCCESS) {
+ //ignore mixed types
+ return CELIX_SUCCESS;
+ }
+
+ celix_array_list_create_options_t opts = CELIX_EMPTY_ARRAY_LIST_CREATE_OPTIONS;
+ opts.elementType = elType;
+ celix_autoptr(celix_array_list_t) array = celix_arrayList_createWithOptions(&opts);
+ if (!array) {
+ return ENOMEM;
+ }
+
+ json_t* value;
+ int index;
+ json_array_foreach(jsonArray, index, value) {
+ switch (elType) {
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING:
+ status = celix_arrayList_addString(array, json_string_value(value));
+ break;
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG:
+ status = celix_arrayList_addLong(array, (long)json_integer_value(value));
+ break;
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE:
+ status = celix_arrayList_addDouble(array, json_number_value(value));
+ break;
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL:
+ status = celix_arrayList_addBool(array, json_boolean_value(value));
+ break;
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: {
+ celix_version_t* v;
+ status = celix_properties_parseVersion(json_string_value(value), &v);
+ status = CELIX_DO_IF(status, celix_arrayList_assignVersion(array, v));
+ break;
+ }
+ default:
+ // LCOV_EXCL_START
+ celix_err_pushf("Unexpected array list element type %d for key %s.", elType, key);
+ return CELIX_ILLEGAL_ARGUMENT;
+ // LCOV_EXCL_STOP
+ }
+ if (status != CELIX_SUCCESS) {
+ return status;
+ }
+ }
+ return celix_properties_assignArrayList(props, key, celix_steal_ptr(array));
+}
+
+static celix_status_t
+celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags) {
+ if (strncmp(key, "", 1) == 0) {
+ if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS) {
+ celix_err_push("Key cannot be empty.");
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ }
+
+ 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;
+ }
+
+ celix_status_t status = CELIX_SUCCESS;
+ if (json_is_string(jsonValue) && celix_properties_isVersionString(json_string_value(jsonValue))) {
+ celix_version_t* version;
+ status = celix_properties_parseVersion(json_string_value(jsonValue), &version);
+ status = CELIX_DO_IF(status, celix_properties_assignVersion(props, key, version));
+ } else if (json_is_string(jsonValue)) {
+ status = celix_properties_setString(props, key, json_string_value(jsonValue));
+ } else if (json_is_integer(jsonValue)) {
+ status = celix_properties_setLong(props, key, json_integer_value(jsonValue));
+ } else if (json_is_real(jsonValue)) {
+ status = celix_properties_setDouble(props, key, json_real_value(jsonValue));
+ } else if (json_is_boolean(jsonValue)) {
+ status = celix_properties_setBool(props, key, json_boolean_value(jsonValue));
+ } else if (json_is_object(jsonValue)) {
+ const char* fieldName;
+ json_t* fieldValue;
+ json_object_foreach(jsonValue, fieldName, fieldValue) {
+ char buf[64];
+ char* combinedKey = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s%c%s", key, CELIX_PROPERTIES_JSONPATH_SEPARATOR, 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 ENOMEM;
+ }
+ status = celix_properties_decodeValue(props, combinedKey, fieldValue, flags);
+ if (status != CELIX_SUCCESS) {
+ return status;
+ }
+ }
+ 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("Invalid empty array for key '%s'.", key);
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ // ignore empty arrays
+ return CELIX_SUCCESS;
+ } else if (json_is_array(jsonValue)) {
+ 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("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);
+ return CELIX_ILLEGAL_ARGUMENT;
+ // LCOV_EXCL_STOP
+ }
+ return status;
+}
+
+static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, celix_properties_t** out) {
+ *out = NULL;
+ if (!json_is_object(obj)) {
+ celix_err_push("Expected json object.");
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ if (!props) {
+ return ENOMEM;
+ }
+
+ const char* key;
+ json_t* value;
+ json_object_foreach(obj, key, value) {
+ celix_status_t status = celix_properties_decodeValue(props, key, value, flags);
+ if (status != CELIX_SUCCESS) {
+ return status;
+ }
+ }
+
+ *out = celix_steal_ptr(props);
+ return CELIX_SUCCESS;
+}
+
+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) {
+ jsonFlags = JSON_REJECT_DUPLICATES;
+ }
+ json_auto_t* root = json_loadf(stream, jsonFlags, &jsonError);
+ if (!root) {
+ celix_err_pushf("Failed to parse json from %s:%i:%i: %s.",
+ jsonError.source,
+ jsonError.line,
+ jsonError.column,
+ jsonError.text);
+ return celix_properties_jsonErrorToStatus(json_error_code(&jsonError));
+ }
+ 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 ENOMEM;
+ }
+ celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out);
+ fclose(stream);
+ return status;
+}
+
+celix_status_t celix_properties_jsonErrorToStatus(enum json_error_code error) {
+ switch (error) {
+ case json_error_unknown:
+ return CELIX_ILLEGAL_STATE;
+ case json_error_out_of_memory:
+ case json_error_stack_overflow:
+ return ENOMEM;
+ case json_error_cannot_open_file:
+ return CELIX_FILE_IO_EXCEPTION;
+ case json_error_invalid_argument:
+ case json_error_invalid_utf8:
+ case json_error_premature_end_of_input:
+ case json_error_end_of_input_expected:
+ case json_error_invalid_syntax:
+ case json_error_invalid_format:
+ case json_error_wrong_type:
+ case json_error_null_character:
+ case json_error_null_value:
+ case json_error_null_byte_in_key:
+ case json_error_duplicate_key:;
+ case json_error_numeric_overflow:
+ case json_error_item_not_found:
+ case json_error_index_out_of_range:
+ default:
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+}