aboutsummaryrefslogtreecommitdiff
path: root/src/backend/utils/adt/jsonfuncs.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/backend/utils/adt/jsonfuncs.c')
-rw-r--r--src/backend/utils/adt/jsonfuncs.c717
1 files changed, 717 insertions, 0 deletions
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index 5f61cfcb73a..2d414551276 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -124,6 +124,21 @@ static JsonbValue *findJsonbValueFromContainerLen(JsonbContainer *container,
char *key,
uint32 keylen);
+/* functions supporting jsonb_delete, jsonb_replace and jsonb_concat */
+static JsonbValue *IteratorConcat(JsonbIterator **it1, JsonbIterator **it2,
+ JsonbParseState **state);
+static JsonbValue *walkJsonb(JsonbIterator **it, JsonbParseState **state, bool stop_at_level_zero);
+static JsonbValue *replacePath(JsonbIterator **it, Datum *path_elems,
+ bool *path_nulls, int path_len,
+ JsonbParseState **st, int level, Jsonb *newval);
+static void replacePathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls,
+ int path_len, JsonbParseState **st, int level,
+ Jsonb *newval, uint32 nelems);
+static void replacePathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls,
+ int path_len, JsonbParseState **st, int level,
+ Jsonb *newval, uint32 npairs);
+static void addJsonbToParseState(JsonbParseState **jbps, Jsonb * jb);
+
/* state for json_object_keys */
typedef struct OkeysState
{
@@ -3199,3 +3214,705 @@ jsonb_strip_nulls(PG_FUNCTION_ARGS)
PG_RETURN_POINTER(JsonbValueToJsonb(res));
}
+
+/*
+ * Add values from the jsonb to the parse state.
+ *
+ * If the parse state container is an object, the jsonb is pushed as
+ * a value, not a key.
+ *
+ * This needs to be done using an iterator because pushJsonbValue doesn't
+ * like getting jbvBinary values, so we can't just push jb as a whole.
+ */
+static void
+addJsonbToParseState(JsonbParseState **jbps, Jsonb * jb)
+{
+
+ JsonbIterator *it;
+ JsonbValue *o = &(*jbps)->contVal;
+ int type;
+ JsonbValue v;
+
+ it = JsonbIteratorInit(&jb->root);
+
+ Assert(o->type == jbvArray || o->type == jbvObject);
+
+ if (JB_ROOT_IS_SCALAR(jb))
+ {
+ (void) JsonbIteratorNext(&it, &v, false); /* skip array header */
+ (void) JsonbIteratorNext(&it, &v, false); /* fetch scalar value */
+
+ switch (o->type)
+ {
+ case jbvArray:
+ (void) pushJsonbValue(jbps, WJB_ELEM, &v);
+ break;
+ case jbvObject:
+ (void) pushJsonbValue(jbps, WJB_VALUE, &v);
+ break;
+ default:
+ elog(ERROR, "unexpected parent of nested structure");
+ }
+ }
+ else
+ {
+ while ((type = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
+ {
+ if (type == WJB_KEY || type == WJB_VALUE || type == WJB_ELEM)
+ (void) pushJsonbValue(jbps, type, &v);
+ else
+ (void) pushJsonbValue(jbps, type, NULL);
+ }
+ }
+
+}
+
+/*
+ * SQL function jsonb_pretty (jsonb)
+ *
+ * Pretty-printed text for the jsonb
+ */
+Datum
+jsonb_pretty(PG_FUNCTION_ARGS)
+{
+ Jsonb *jb = PG_GETARG_JSONB(0);
+ StringInfo str = makeStringInfo();
+
+ JsonbToCStringIndent(str, &jb->root, VARSIZE(jb));
+
+ PG_RETURN_TEXT_P(cstring_to_text_with_len(str->data, str->len));
+}
+
+
+/*
+ * SQL function jsonb_concat (jsonb, jsonb)
+ *
+ * function for || operator
+ */
+Datum
+jsonb_concat(PG_FUNCTION_ARGS)
+{
+ Jsonb *jb1 = PG_GETARG_JSONB(0);
+ Jsonb *jb2 = PG_GETARG_JSONB(1);
+ Jsonb *out = palloc(VARSIZE(jb1) + VARSIZE(jb2));
+ JsonbParseState *state = NULL;
+ JsonbValue *res;
+ JsonbIterator *it1,
+ *it2;
+
+ /*
+ * If one of the jsonb is empty, just return other.
+ */
+ if (JB_ROOT_COUNT(jb1) == 0)
+ {
+ memcpy(out, jb2, VARSIZE(jb2));
+ PG_RETURN_POINTER(out);
+ }
+ else if (JB_ROOT_COUNT(jb2) == 0)
+ {
+ memcpy(out, jb1, VARSIZE(jb1));
+ PG_RETURN_POINTER(out);
+ }
+
+ it1 = JsonbIteratorInit(&jb1->root);
+ it2 = JsonbIteratorInit(&jb2->root);
+
+ res = IteratorConcat(&it1, &it2, &state);
+
+ if (res == NULL || (res->type == jbvArray && res->val.array.nElems == 0) ||
+ (res->type == jbvObject && res->val.object.nPairs == 0))
+ {
+ SET_VARSIZE(out, VARHDRSZ);
+ }
+ else
+ {
+ if (res->type == jbvArray && res->val.array.nElems > 1)
+ res->val.array.rawScalar = false;
+
+ out = JsonbValueToJsonb(res);
+ }
+
+ PG_RETURN_POINTER(out);
+}
+
+
+/*
+ * SQL function jsonb_delete (jsonb, text)
+ *
+ * return a copy of the jsonb with the indicated item
+ * removed.
+ */
+Datum
+jsonb_delete(PG_FUNCTION_ARGS)
+{
+ Jsonb *in = PG_GETARG_JSONB(0);
+ text *key = PG_GETARG_TEXT_PP(1);
+ char *keyptr = VARDATA_ANY(key);
+ int keylen = VARSIZE_ANY_EXHDR(key);
+ Jsonb *out = palloc(VARSIZE(in));
+ JsonbParseState *state = NULL;
+ JsonbIterator *it;
+ uint32 r;
+ JsonbValue v,
+ *res = NULL;
+ bool skipNested = false;
+
+ SET_VARSIZE(out, VARSIZE(in));
+
+ if (JB_ROOT_COUNT(in) == 0)
+ PG_RETURN_POINTER(out);
+
+ it = JsonbIteratorInit(&in->root);
+
+ while ((r = JsonbIteratorNext(&it, &v, skipNested)) != 0)
+ {
+ skipNested = true;
+
+ if ((r == WJB_ELEM || r == WJB_KEY) &&
+ (v.type == jbvString && keylen == v.val.string.len &&
+ memcmp(keyptr, v.val.string.val, keylen) == 0))
+ {
+ /* skip corresponding value as well */
+ if (r == WJB_KEY)
+ JsonbIteratorNext(&it, &v, true);
+
+ continue;
+ }
+
+ res = pushJsonbValue(&state, r, r < WJB_BEGIN_ARRAY ? &v : NULL);
+ }
+
+ if (res == NULL || (res->type == jbvArray && res->val.array.nElems == 0) ||
+ (res->type == jbvObject && res->val.object.nPairs == 0))
+ SET_VARSIZE(out, VARHDRSZ);
+ else
+ out = JsonbValueToJsonb(res);
+
+ PG_RETURN_POINTER(out);
+}
+
+/*
+ * SQL function jsonb_delete (jsonb, int)
+ *
+ * return a copy of the jsonb with the indicated item
+ * removed. Negative int means count back from the
+ * end of the items.
+ */
+Datum
+jsonb_delete_idx(PG_FUNCTION_ARGS)
+{
+ Jsonb *in = PG_GETARG_JSONB(0);
+ int idx = PG_GETARG_INT32(1);
+ Jsonb *out = palloc(VARSIZE(in));
+ JsonbParseState *state = NULL;
+ JsonbIterator *it;
+ uint32 r,
+ i = 0,
+ n;
+ JsonbValue v,
+ *res = NULL;
+
+ if (JB_ROOT_COUNT(in) == 0)
+ {
+ memcpy(out, in, VARSIZE(in));
+ PG_RETURN_POINTER(out);
+ }
+
+ it = JsonbIteratorInit(&in->root);
+
+ r = JsonbIteratorNext(&it, &v, false);
+ if (r == WJB_BEGIN_ARRAY)
+ n = v.val.array.nElems;
+ else
+ n = v.val.object.nPairs;
+
+ if (idx < 0)
+ {
+ if (-idx > n)
+ idx = n;
+ else
+ idx = n + idx;
+ }
+
+ if (idx >= n)
+ {
+ memcpy(out, in, VARSIZE(in));
+ PG_RETURN_POINTER(out);
+ }
+
+ pushJsonbValue(&state, r, r < WJB_BEGIN_ARRAY ? &v : NULL);
+
+ while ((r = JsonbIteratorNext(&it, &v, true)) != 0)
+ {
+ if (r == WJB_ELEM || r == WJB_KEY)
+ {
+ if (i++ == idx)
+ {
+ if (r == WJB_KEY)
+ JsonbIteratorNext(&it, &v, true); /* skip value */
+ continue;
+ }
+ }
+
+ res = pushJsonbValue(&state, r, r < WJB_BEGIN_ARRAY ? &v : NULL);
+ }
+
+ if (res == NULL || (res->type == jbvArray && res->val.array.nElems == 0) ||
+ (res->type == jbvObject && res->val.object.nPairs == 0))
+ SET_VARSIZE(out, VARHDRSZ);
+ else
+ out = JsonbValueToJsonb(res);
+
+ PG_RETURN_POINTER(out);
+}
+
+/*
+ * SQL function jsonb_replace(jsonb, text[], jsonb)
+ */
+Datum
+jsonb_replace(PG_FUNCTION_ARGS)
+{
+ Jsonb *in = PG_GETARG_JSONB(0);
+ ArrayType *path = PG_GETARG_ARRAYTYPE_P(1);
+ Jsonb *newval = PG_GETARG_JSONB(2);
+ Jsonb *out = palloc(VARSIZE(in) + VARSIZE(newval));
+ JsonbValue *res = NULL;
+ Datum *path_elems;
+ bool *path_nulls;
+ int path_len;
+ JsonbIterator *it;
+ JsonbParseState *st = NULL;
+
+ if (ARR_NDIM(path) > 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+ errmsg("wrong number of array subscripts")));
+
+ if (JB_ROOT_COUNT(in) == 0)
+ {
+ memcpy(out, in, VARSIZE(in));
+ PG_RETURN_POINTER(out);
+ }
+
+ deconstruct_array(path, TEXTOID, -1, false, 'i',
+ &path_elems, &path_nulls, &path_len);
+
+ if (path_len == 0)
+ {
+ memcpy(out, in, VARSIZE(in));
+ PG_RETURN_POINTER(out);
+ }
+
+ it = JsonbIteratorInit(&in->root);
+
+ res = replacePath(&it, path_elems, path_nulls, path_len, &st, 0, newval);
+
+ if (res == NULL)
+ SET_VARSIZE(out, VARHDRSZ);
+ else
+ out = JsonbValueToJsonb(res);
+
+ PG_RETURN_POINTER(out);
+}
+
+
+/*
+ * SQL function jsonb_delete(jsonb, text[])
+ */
+Datum
+jsonb_delete_path(PG_FUNCTION_ARGS)
+{
+ Jsonb *in = PG_GETARG_JSONB(0);
+ ArrayType *path = PG_GETARG_ARRAYTYPE_P(1);
+ Jsonb *out = palloc(VARSIZE(in));
+ JsonbValue *res = NULL;
+ Datum *path_elems;
+ bool *path_nulls;
+ int path_len;
+ JsonbIterator *it;
+ JsonbParseState *st = NULL;
+
+ if (ARR_NDIM(path) > 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+ errmsg("wrong number of array subscripts")));
+
+ if (JB_ROOT_COUNT(in) == 0)
+ {
+ memcpy(out, in, VARSIZE(in));
+ PG_RETURN_POINTER(out);
+ }
+
+ deconstruct_array(path, TEXTOID, -1, false, 'i',
+ &path_elems, &path_nulls, &path_len);
+
+ if (path_len == 0)
+ {
+ memcpy(out, in, VARSIZE(in));
+ PG_RETURN_POINTER(out);
+ }
+
+ it = JsonbIteratorInit(&in->root);
+
+ res = replacePath(&it, path_elems, path_nulls, path_len, &st, 0, NULL);
+
+ if (res == NULL)
+ SET_VARSIZE(out, VARHDRSZ);
+ else
+ out = JsonbValueToJsonb(res);
+
+ PG_RETURN_POINTER(out);
+}
+
+
+/*
+ * Iterate over all jsonb objects and merge them into one.
+ * The logic of this function copied from the same hstore function,
+ * except the case, when it1 & it2 represents jbvObject.
+ * In that case we just append the content of it2 to it1 without any
+ * verifications.
+ */
+static JsonbValue *
+IteratorConcat(JsonbIterator **it1, JsonbIterator **it2,
+ JsonbParseState **state)
+{
+ uint32 r1,
+ r2,
+ rk1,
+ rk2;
+ JsonbValue v1,
+ v2,
+ *res = NULL;
+
+ r1 = rk1 = JsonbIteratorNext(it1, &v1, false);
+ r2 = rk2 = JsonbIteratorNext(it2, &v2, false);
+
+ /*
+ * Both elements are objects.
+ */
+ if (rk1 == WJB_BEGIN_OBJECT && rk2 == WJB_BEGIN_OBJECT)
+ {
+ int level = 1;
+
+ /*
+ * Append the all tokens from v1 to res, exept last WJB_END_OBJECT
+ * (because res will not be finished yet).
+ */
+ (void) pushJsonbValue(state, r1, NULL);
+ while ((r1 = JsonbIteratorNext(it1, &v1, false)) != 0)
+ {
+ if (r1 == WJB_BEGIN_OBJECT)
+ ++level;
+ else if (r1 == WJB_END_OBJECT)
+ --level;
+
+ if (level != 0)
+ res = pushJsonbValue(state, r1, r1 < WJB_BEGIN_ARRAY ? &v1 : NULL);
+ }
+
+ /*
+ * Append the all tokens from v2 to res, include last WJB_END_OBJECT
+ * (the concatenation will be completed).
+ */
+ while ((r2 = JsonbIteratorNext(it2, &v2, false)) != 0)
+ res = pushJsonbValue(state, r2, r2 < WJB_BEGIN_ARRAY ? &v2 : NULL);
+ }
+
+ /*
+ * Both elements are arrays (either can be scalar).
+ */
+ else if (rk1 == WJB_BEGIN_ARRAY && rk2 == WJB_BEGIN_ARRAY)
+ {
+ res = pushJsonbValue(state, r1, NULL);
+ for (;;)
+ {
+ r1 = JsonbIteratorNext(it1, &v1, true);
+ if (r1 == WJB_END_OBJECT || r1 == WJB_END_ARRAY)
+ break;
+
+ Assert(r1 == WJB_KEY || r1 == WJB_VALUE || r1 == WJB_ELEM);
+ pushJsonbValue(state, r1, &v1);
+ }
+
+ while ((r2 = JsonbIteratorNext(it2, &v2, true)) != 0)
+ {
+ if (!(r2 == WJB_END_OBJECT || r2 == WJB_END_ARRAY))
+ {
+ if (rk1 == WJB_BEGIN_OBJECT)
+ {
+ pushJsonbValue(state, WJB_KEY, NULL);
+ r2 = JsonbIteratorNext(it2, &v2, true);
+ Assert(r2 == WJB_ELEM);
+ pushJsonbValue(state, WJB_VALUE, &v2);
+ }
+ else
+ pushJsonbValue(state, WJB_ELEM, &v2);
+ }
+ }
+
+ res = pushJsonbValue(state,
+ (rk1 == WJB_BEGIN_OBJECT) ? WJB_END_OBJECT : WJB_END_ARRAY,
+ NULL /* signal to sort */ );
+ }
+ /* have we got array || object or object || array? */
+ else if (((rk1 == WJB_BEGIN_ARRAY && !(*it1)->isScalar) && rk2 == WJB_BEGIN_OBJECT) ||
+ (rk1 == WJB_BEGIN_OBJECT && (rk2 == WJB_BEGIN_ARRAY && !(*it2)->isScalar)))
+ {
+
+ JsonbIterator **it_array = rk1 == WJB_BEGIN_ARRAY ? it1 : it2;
+ JsonbIterator **it_object = rk1 == WJB_BEGIN_OBJECT ? it1 : it2;
+
+ bool prepend = (rk1 == WJB_BEGIN_OBJECT) ? true : false;
+
+ pushJsonbValue(state, WJB_BEGIN_ARRAY, NULL);
+ if (prepend)
+ {
+ pushJsonbValue(state, WJB_BEGIN_OBJECT, NULL);
+ walkJsonb(it_object, state, false);
+
+ res = walkJsonb(it_array, state, false);
+ }
+ else
+ {
+ walkJsonb(it_array, state, true);
+
+ pushJsonbValue(state, WJB_BEGIN_OBJECT, NULL);
+ walkJsonb(it_object, state, false);
+
+ res = pushJsonbValue(state, WJB_END_ARRAY, NULL);
+ }
+ }
+ else
+ {
+ /*
+ * This must be scalar || object or object || scalar, as that's all
+ * that's left. Both of these make no sense, so error out.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid concatenation of jsonb objects")));
+ }
+
+ return res;
+}
+
+/*
+ * copy elements from the iterator to the parse state
+ * stopping at level zero if required.
+ */
+static JsonbValue *
+walkJsonb(JsonbIterator **it, JsonbParseState **state, bool stop_at_level_zero)
+{
+ uint32 r,
+ level = 1;
+ JsonbValue v;
+ JsonbValue *res = NULL;
+
+ while ((r = JsonbIteratorNext(it, &v, false)) != WJB_DONE)
+ {
+ if (r == WJB_BEGIN_OBJECT || r == WJB_BEGIN_ARRAY)
+ ++level;
+ else if (r == WJB_END_OBJECT || r == WJB_END_ARRAY)
+ --level;
+
+ if (stop_at_level_zero && level == 0)
+ break;
+
+ res = pushJsonbValue(state, r, r < WJB_BEGIN_ARRAY ? &v : NULL);
+ }
+
+ return res;
+}
+
+
+/*
+ * do most of the heavy work for jsonb_replace
+ */
+static JsonbValue *
+replacePath(JsonbIterator **it, Datum *path_elems,
+ bool *path_nulls, int path_len,
+ JsonbParseState **st, int level, Jsonb *newval)
+{
+ JsonbValue v;
+ JsonbValue *res = NULL;
+ int r;
+
+ r = JsonbIteratorNext(it, &v, false);
+
+ switch (r)
+ {
+ case WJB_BEGIN_ARRAY:
+ (void) pushJsonbValue(st, r, NULL);
+ replacePathArray(it, path_elems, path_nulls, path_len, st, level,
+ newval, v.val.array.nElems);
+ r = JsonbIteratorNext(it, &v, false);
+ Assert(r == WJB_END_ARRAY);
+ res = pushJsonbValue(st, r, NULL);
+
+ break;
+ case WJB_BEGIN_OBJECT:
+ (void) pushJsonbValue(st, r, NULL);
+ replacePathObject(it, path_elems, path_nulls, path_len, st, level,
+ newval, v.val.object.nPairs);
+ r = JsonbIteratorNext(it, &v, true);
+ Assert(r == WJB_END_OBJECT);
+ res = pushJsonbValue(st, r, NULL);
+
+ break;
+ case WJB_ELEM:
+ case WJB_VALUE:
+ res = pushJsonbValue(st, r, &v);
+ break;
+ default:
+ elog(ERROR, "impossible state");
+ }
+
+ return res;
+}
+
+/*
+ * Object walker for replacePath
+ */
+static void
+replacePathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls,
+ int path_len, JsonbParseState **st, int level,
+ Jsonb *newval, uint32 nelems)
+{
+ JsonbValue v;
+ int i;
+ JsonbValue k;
+ bool done = false;
+
+ if (level >= path_len || path_nulls[level])
+ done = true;
+
+ for (i = 0; i < nelems; i++)
+ {
+ int r = JsonbIteratorNext(it, &k, true);
+ Assert(r == WJB_KEY);
+
+ if (!done &&
+ k.val.string.len == VARSIZE_ANY_EXHDR(path_elems[level]) &&
+ memcmp(k.val.string.val, VARDATA_ANY(path_elems[level]),
+ k.val.string.len) == 0)
+ {
+ if (level == path_len - 1)
+ {
+ r = JsonbIteratorNext(it, &v, true); /* skip */
+ if (newval != NULL)
+ {
+ (void) pushJsonbValue(st, WJB_KEY, &k);
+ addJsonbToParseState(st, newval);
+ }
+ }
+ else
+ {
+ (void) pushJsonbValue(st, r, &k);
+ replacePath(it, path_elems, path_nulls, path_len,
+ st, level + 1, newval);
+ }
+ }
+ else
+ {
+ (void) pushJsonbValue(st, r, &k);
+ r = JsonbIteratorNext(it, &v, false);
+ (void) pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL);
+ if (r == WJB_BEGIN_ARRAY || r == WJB_BEGIN_OBJECT)
+ {
+ int walking_level = 1;
+
+ while (walking_level != 0)
+ {
+ r = JsonbIteratorNext(it, &v, false);
+
+ if (r == WJB_BEGIN_ARRAY || r == WJB_BEGIN_OBJECT)
+ ++walking_level;
+ if (r == WJB_END_ARRAY || r == WJB_END_OBJECT)
+ --walking_level;
+
+ (void) pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL);
+ }
+ }
+ }
+ }
+}
+
+/*
+ * Array walker for replacePath
+ */
+static void
+replacePathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls,
+ int path_len, JsonbParseState **st, int level,
+ Jsonb *newval, uint32 npairs)
+{
+ JsonbValue v;
+ int idx,
+ i;
+ char *badp;
+
+ /* pick correct index */
+ if (level < path_len && !path_nulls[level])
+ {
+ char *c = VARDATA_ANY(path_elems[level]);
+
+ errno = 0;
+ idx = (int) strtol(c, &badp, 10);
+ if (errno != 0 || badp == c)
+ idx = npairs;
+ }
+ else
+ idx = npairs;
+
+ if (idx < 0)
+ {
+ if (-idx > npairs)
+ idx = npairs;
+ else
+ idx = npairs + idx;
+ }
+
+ if (idx > npairs)
+ idx = npairs;
+
+ /* iterate over the array elements */
+ for (i = 0; i < npairs; i++)
+ {
+ int r;
+
+ if (i == idx && level < path_len)
+ {
+ if (level == path_len - 1)
+ {
+ r = JsonbIteratorNext(it, &v, true); /* skip */
+ if (newval != NULL)
+ addJsonbToParseState(st, newval);
+ }
+ else
+ (void) replacePath(it, path_elems, path_nulls, path_len,
+ st, level + 1, newval);
+ }
+ else
+ {
+ r = JsonbIteratorNext(it, &v, false);
+
+ (void) pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL);
+
+ if (r == WJB_BEGIN_ARRAY || r == WJB_BEGIN_OBJECT)
+ {
+ int walking_level = 1;
+
+ while (walking_level != 0)
+ {
+ r = JsonbIteratorNext(it, &v, false);
+
+ if (r == WJB_BEGIN_ARRAY || r == WJB_BEGIN_OBJECT)
+ ++walking_level;
+ if (r == WJB_END_ARRAY || r == WJB_END_OBJECT)
+ --walking_level;
+
+ (void) pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL);
+ }
+ }
+ }
+ }
+}