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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2012-2016 ForgeRock AS.
* Portions copyright 2020-2026 3A Systems, LLC
*/

package org.forgerock.json.resource;
Expand All @@ -28,7 +29,6 @@
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import org.forgerock.api.annotations.CollectionProvider;
import org.forgerock.api.annotations.Path;
Expand Down Expand Up @@ -99,6 +99,15 @@ public static RequestHandler asRequestHandler(final SynchronousRequestHandler sy
* <b>NOTE:</b> this method only performs a shallow copy of extracted
* fields, so changes to the filtered JSON value may impact the original
* JSON value, and vice-versa.
* <p>
* The projection preserves the nested structure of the requested fields:
* each requested {@link JsonPointer} is written back under its full path
* rather than being collapsed to its leaf name. As a result, leaf fields
* that share the same name at different levels of nesting do not conflict.
* For example, projecting {@code userName} and {@code manager/userName}
* over <code>{ "userName": "bjensen", "manager": { "userName": "jdoe" } }</code>
* yields <code>{ "userName": "bjensen", "manager": { "userName": "jdoe" } }</code>,
* i.e. the top-level {@code userName} is not overwritten by the nested one.
*
* @param resource
* The JSON value whose fields are to be filtered.
Expand All @@ -111,21 +120,24 @@ public static JsonValue filterResource(final JsonValue resource,
if (fields.isEmpty() || resource.isNull() || resource.size() == 0) {
return resource;
} else {
final Map<String, Object> filtered = new LinkedHashMap<>(fields.size());
final JsonValue filtered = new JsonValue(new LinkedHashMap<String, Object>(fields.size()));
for (final JsonPointer field : fields) {
if (field.isEmpty()) {
// Special case - copy resource fields (assumes Map).
filtered.putAll(resource.asMap());
filtered.asMap().putAll(resource.asMap());
} else {
// FIXME: what should we do if the field refers to an array element?
final JsonValue value = resource.get(field);
if (value != null) {
final String key = field.leaf();
filtered.put(key, value.getObject());
// Preserve the nested structure of the requested field
// instead of collapsing it to its leaf name. This keeps
// same-named leaf fields at different levels of nesting
// (e.g. "userName" and "manager/userName") from
// overwriting each other.
filtered.putPermissive(field, value.getObject());
}
}
}
return new JsonValue(filtered);
return filtered;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2012-2016 ForgeRock AS.
* Portions copyright 2020-2026 3A Systems, LLC
*/

package org.forgerock.json.resource;
Expand Down Expand Up @@ -161,25 +162,36 @@ public Object[][] testFilterData() {
{
filter("/a/b"),
content(object(field("a", object(field("b", "1"), field("c", "2"))), field("d", "3"))),
expected(object(field("b", "1")))
expected(object(field("a", object(field("b", "1")))))
},

{
filter("/a/b", "/d"),
content(object(field("a", object(field("b", "1"), field("c", "2"))), field("d", "3"))),
expected(object(field("b", "1"), field("d", "3")))
expected(object(field("a", object(field("b", "1"))), field("d", "3")))
},

{
filter("/a/b", "/a"),
content(object(field("a", object(field("b", "1"), field("c", "2"))), field("d", "3"))),
expected(object(field("b", "1"), field("a", object(field("b", "1"), field("c", "2")))))
expected(object(field("a", object(field("b", "1"), field("c", "2")))))
},

{
filter("/a", "/a/b"),
content(object(field("a", object(field("b", "1"), field("c", "2"))), field("d", "3"))),
expected(object(field("a", object(field("b", "1"), field("c", "2"))), field("b", "1")))
expected(object(field("a", object(field("b", "1"), field("c", "2")))))
},

// Same-named leaf fields at different levels of nesting must coexist
// (see OpenIDM discussion #183): the top-level "userName" must not be
// overwritten by the nested "manager/userName".
{
filter("/userName", "/manager", "/manager/userName"),
content(object(field("userName", "bjensen"),
field("manager", object(field("userName", "jdoe"))))),
expected(object(field("userName", "bjensen"),
field("manager", object(field("userName", "jdoe")))))
},

};
Expand All @@ -192,6 +204,23 @@ public void testFilter(List<JsonPointer> filter, JsonValue content, JsonValue ex
expected.getObject());
}

@Test
public void testFilterPreservesNestedFieldsWithCollidingLeafNames() {
// Given a resource with a "userName" both at the top level and nested
// inside "manager" (see OpenIDM discussion #183).
final JsonValue content = content(object(
field("userName", "bjensen"),
field("manager", object(field("userName", "jdoe")))));

// When projecting userName, manager and manager/userName.
final JsonValue result = Resources.filterResource(content,
filter("/userName", "/manager", "/manager/userName"));

// Then the nested structure is preserved and the leaf names do not collide.
assertThat(result.get("userName").asString()).isEqualTo("bjensen");
assertThat(result.get("manager").get("userName").asString()).isEqualTo("jdoe");
}

@DataProvider
public Object[][] testCollectionResourceProviderData() {
// @formatter:off
Expand Down
Loading