Skip to content

Commit fe9db0f

Browse files
feat: support nested field paths in field filtering
1 parent 64c66f0 commit fe9db0f

6 files changed

Lines changed: 148 additions & 30 deletions

File tree

sda-commons-server-jackson/src/main/java/org/sdase/commons/server/jackson/filter/FieldFilterSerializerModifier.java

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase;
1313
import jakarta.ws.rs.core.Context;
1414
import jakarta.ws.rs.core.UriInfo;
15-
import java.util.LinkedHashSet;
15+
import java.util.ArrayList;
1616
import java.util.List;
17-
import java.util.Set;
1817
import java.util.stream.Stream;
1918
import org.sdase.commons.server.jackson.EnableFieldFilter;
2019

@@ -26,9 +25,9 @@
2625
*
2726
* <ol>
2827
* <li>The property is returned if no field filter is set
29-
* <li>The property is <b>not</b> returned if the field (at the top level) is <b>not</b> part of
30-
* the set of filtered fields ({@code &fields=} parameter)
31-
* <li>The property is returned if it is part of a nested or embedded object
28+
* <li>The property is <b>not</b> returned if the field path is <b>not</b> part of the filtered
29+
* fields ({@code &fields=} parameter)
30+
* <li>The property is returned if it is part of an embedded object
3231
* </ol>
3332
*/
3433
public class FieldFilterSerializerModifier extends BeanSerializerModifier {
@@ -69,17 +68,15 @@ private static class SkipFieldBeanPropertyWriter extends BeanPropertyWriter {
6968
@Override
7069
public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov)
7170
throws Exception {
72-
if (!hasAnyFieldFilter() || isIncludedField() || isEmbeddedOrNested(gen)) {
71+
List<String> currentFieldPath =
72+
getPath(gen.getOutputContext(), getName(), new ArrayList<>());
73+
if (!hasAnyFieldFilter()
74+
|| isIncludedField(currentFieldPath)
75+
|| isEmbedded(currentFieldPath)) {
7376
super.serializeAsField(bean, gen, prov);
7477
}
7578
}
7679

77-
private boolean isEmbeddedOrNested(JsonGenerator generator) {
78-
Set<String> pathSet =
79-
getPath(generator.getOutputContext(), getName(), new LinkedHashSet<>());
80-
return isEmbedded(pathSet) || isNested(pathSet);
81-
}
82-
8380
private boolean hasAnyFieldFilter() {
8481
try {
8582
List<String> fieldFilters = uriInfo.getQueryParameters().get(FIELD_FILTER_QUERY_PARAM);
@@ -90,33 +87,39 @@ private boolean hasAnyFieldFilter() {
9087
}
9188
}
9289

93-
private boolean isIncludedField() {
90+
private boolean isIncludedField(List<String> currentPath) {
9491
Stream<String> requestedFields =
9592
uriInfo.getQueryParameters().get(FIELD_FILTER_QUERY_PARAM).stream()
9693
.map(fields -> fields.split(","))
9794
.flatMap(Stream::of)
98-
.map(String::trim);
99-
return requestedFields.anyMatch(fieldName -> fieldName.equals(getName()));
95+
.map(String::trim)
96+
.filter(fieldName -> !fieldName.isEmpty());
97+
String serializedPath = String.join(".", currentPath);
98+
return requestedFields.anyMatch(requestedPath -> matches(requestedPath, serializedPath));
10099
}
101100

102101
/**
103-
* Checks if the property is part of a nested object
102+
* Checks if the object is part to an embedded object
104103
*
105-
* @param set the full path of the object
106-
* @return true if the property is part of a nested object
104+
* @param path the full path of the object
105+
* @return true if the object is part to an embedded object
107106
*/
108-
private boolean isNested(Set<String> set) {
109-
return set.size() > 1;
107+
private boolean isEmbedded(List<String> path) {
108+
return path.contains("_embedded");
110109
}
111110

112111
/**
113-
* Checks if the object is part to an embedded object
112+
* Checks whether the current path should be included for a requested path.
114113
*
115-
* @param set the full path of the object
116-
* @return true if the object is part to an embedded object
114+
* @param requestedPath the field path that should be included
115+
* @param currentPath the serialized field path currently being evaluated
116+
* @return true if the paths match exactly, if the current path is a parent of the requested
117+
* path, or if the current path is a child of the requested path
117118
*/
118-
private boolean isEmbedded(Set<String> set) {
119-
return set.contains("_embedded");
119+
private boolean matches(String requestedPath, String currentPath) {
120+
return currentPath.equals(requestedPath)
121+
|| requestedPath.startsWith(currentPath + ".")
122+
|| currentPath.startsWith(requestedPath + ".");
120123
}
121124

122125
/**
@@ -127,22 +130,22 @@ private boolean isEmbedded(Set<String> set) {
127130
* is used. This might
128131
* @return the path segments from root -> [parent_node] -> ... -> your property
129132
*/
130-
private Set<String> getPath(JsonStreamContext context, String name, Set<String> set) {
133+
private List<String> getPath(JsonStreamContext context, String name, List<String> path) {
131134
JsonStreamContext parentContext = context.getParent();
132135

133136
// Add all parent paths
134137
if (parentContext != null) {
135-
getPath(parentContext, null, set);
138+
getPath(parentContext, null, path);
136139
}
137140

138141
// add the provided name or read from the properties
139142
if (name != null) {
140-
set.add(name);
143+
path.add(name);
141144
} else if (context.getCurrentName() != null) {
142-
set.add(context.getCurrentName());
145+
path.add(context.getCurrentName());
143146
}
144147

145-
return set;
148+
return path;
146149
}
147150
}
148151
}

sda-commons-server-jackson/src/test/java/org/sdase/commons/server/jackson/JacksonConfigurationBundleIT.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.assertj.core.api.Assertions.assertThat;
66
import static org.assertj.core.api.Assertions.tuple;
77

8+
import com.fasterxml.jackson.databind.JsonNode;
89
import io.dropwizard.core.Configuration;
910
import io.dropwizard.testing.junit5.DropwizardAppExtension;
1011
import jakarta.ws.rs.WebApplicationException;
@@ -485,6 +486,83 @@ void shouldFilterNickName() {
485486
"http://localhost:" + DW.getLocalPort() + "/people/jdoe", null, null, "Johnny", null);
486487
}
487488

489+
@Test
490+
void shouldFilterChildrenAndNestedFieldsByPath() throws Exception {
491+
JsonNode johnny =
492+
DropwizardLegacyHelper.client(DW.getObjectMapper())
493+
.target("http://localhost:" + DW.getLocalPort())
494+
.path("people")
495+
.path("jdoe-and-children")
496+
.queryParam(
497+
"fields",
498+
"children.nickName,renamedCustomProp.myNestedResource.anotherNestedField,address.city")
499+
.request(MediaType.APPLICATION_JSON)
500+
.get(JsonNode.class);
501+
502+
assertThat(johnny.has("_links")).isTrue();
503+
assertThat(johnny.has("children")).isTrue();
504+
assertThat(johnny.has("renamedCustomProp")).isTrue();
505+
assertThat(johnny.has("address")).isTrue();
506+
507+
assertThat(johnny.has("firstName")).isFalse();
508+
assertThat(johnny.has("lastName")).isFalse();
509+
assertThat(johnny.has("nickName")).isFalse();
510+
511+
JsonNode child = johnny.path("children").get(0);
512+
assertThat(child.path("nickName").asText()).isEqualTo("Yassie");
513+
assertThat(child.has("_links")).isTrue();
514+
assertThat(child.has("firstName")).isFalse();
515+
assertThat(child.has("lastName")).isFalse();
516+
517+
JsonNode customNested = johnny.path("renamedCustomProp");
518+
assertThat(customNested.size()).isEqualTo(1);
519+
assertThat(customNested.has("myNestedResource")).isTrue();
520+
521+
JsonNode nestedResource = customNested.path("myNestedResource");
522+
assertThat(nestedResource.size()).isEqualTo(1);
523+
assertThat(nestedResource.path("anotherNestedField").asText()).isEqualTo("deep");
524+
assertThat(nestedResource.has("someNumber")).isFalse();
525+
526+
assertThat(johnny.path("address").size()).isEqualTo(1);
527+
assertThat(johnny.path("address").path("city").asText()).isEqualTo("Hamburg");
528+
}
529+
530+
@Test
531+
void shouldKeepFullSubtreeForParentPathsFromRepeatedFieldParams() throws Exception {
532+
JsonNode johnny =
533+
DropwizardLegacyHelper.client(DW.getObjectMapper())
534+
.target("http://localhost:" + DW.getLocalPort())
535+
.path("people")
536+
.path("jdoe-and-children")
537+
.queryParam("fields", "children")
538+
.queryParam("fields", "renamedCustomProp")
539+
.request(MediaType.APPLICATION_JSON)
540+
.get(JsonNode.class);
541+
542+
assertThat(johnny.has("_links")).isTrue();
543+
assertThat(johnny.has("children")).isTrue();
544+
assertThat(johnny.has("renamedCustomProp")).isTrue();
545+
assertThat(johnny.has("firstName")).isFalse();
546+
assertThat(johnny.has("lastName")).isFalse();
547+
assertThat(johnny.has("nickName")).isFalse();
548+
assertThat(johnny.has("address")).isFalse();
549+
550+
JsonNode child = johnny.path("children").get(0);
551+
assertThat(child.has("_links")).isTrue();
552+
assertThat(child.path("firstName").asText()).isEqualTo("Yasmine");
553+
assertThat(child.path("lastName").asText()).isEqualTo("Doe");
554+
assertThat(child.path("nickName").asText()).isEqualTo("Yassie");
555+
556+
JsonNode customNested = johnny.path("renamedCustomProp");
557+
assertThat(customNested.has("myNestedField")).isTrue();
558+
assertThat(customNested.has("someNumber")).isTrue();
559+
assertThat(customNested.has("myNestedResource")).isTrue();
560+
561+
JsonNode nestedResource = customNested.path("myNestedResource");
562+
assertThat(nestedResource.has("anotherNestedField")).isTrue();
563+
assertThat(nestedResource.has("someNumber")).isTrue();
564+
}
565+
488566
@Test
489567
void shouldFilterNickNameInList() {
490568
List<PersonWithChildrenResource> people =

sda-commons-server-jackson/src/test/java/org/sdase/commons/server/jackson/test/JacksonConfigurationTestApp.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ public PersonWithChildrenResource getJohnDoeWithChildren() {
9090
.setFirstName("John")
9191
.setLastName("Doe")
9292
.setNickName("Johnny")
93+
.setAddress(new Address().setCity("Hamburg").setId("Hamburg"))
94+
.setNestedResource(
95+
new NestedResource()
96+
.setAnotherNestedResource(new NestedNestedResource().setAnotherNested("deep")))
9397
.setSelf(new HALLink.Builder(self).build());
9498
john.setChildren(
9599
singletonList(

sda-commons-server-jackson/src/test/java/org/sdase/commons/server/jackson/test/NestedNestedResource.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import com.fasterxml.jackson.annotation.JsonProperty;
44
import jakarta.validation.constraints.NotEmpty;
5+
import org.sdase.commons.server.jackson.EnableFieldFilter;
56

7+
@EnableFieldFilter
68
public class NestedNestedResource {
79

810
@NotEmpty()
@@ -11,4 +13,9 @@ public class NestedNestedResource {
1113

1214
@JsonProperty("someNumber")
1315
private int anotherNumber;
16+
17+
public NestedNestedResource setAnotherNested(String anotherNested) {
18+
this.anotherNested = anotherNested;
19+
return this;
20+
}
1421
}

sda-commons-server-jackson/src/test/java/org/sdase/commons/server/jackson/test/NestedResource.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import com.fasterxml.jackson.annotation.JsonProperty;
44
import jakarta.validation.Valid;
55
import jakarta.validation.constraints.NotEmpty;
6+
import org.sdase.commons.server.jackson.EnableFieldFilter;
67

8+
@EnableFieldFilter
79
public class NestedResource {
810

911
@NotEmpty()

sda-commons-server-jackson/src/test/java/org/sdase/commons/server/jackson/test/PersonWithChildrenResource.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.sdase.commons.server.jackson.test;
22

3+
import com.fasterxml.jackson.annotation.JsonProperty;
34
import io.openapitools.jackson.dataformat.hal.HALLink;
45
import io.openapitools.jackson.dataformat.hal.annotation.Link;
56
import io.openapitools.jackson.dataformat.hal.annotation.Resource;
@@ -19,6 +20,11 @@ public class PersonWithChildrenResource {
1920

2021
private String nickName;
2122

23+
private Address address;
24+
25+
@JsonProperty("renamedCustomProp")
26+
private NestedResource nestedResource;
27+
2228
private List<PersonResource> children;
2329

2430
public HALLink getSelf() {
@@ -57,6 +63,24 @@ public PersonWithChildrenResource setNickName(String nickName) {
5763
return this;
5864
}
5965

66+
public Address getAddress() {
67+
return address;
68+
}
69+
70+
public PersonWithChildrenResource setAddress(Address address) {
71+
this.address = address;
72+
return this;
73+
}
74+
75+
public NestedResource getNestedResource() {
76+
return nestedResource;
77+
}
78+
79+
public PersonWithChildrenResource setNestedResource(NestedResource nestedResource) {
80+
this.nestedResource = nestedResource;
81+
return this;
82+
}
83+
6084
public List<PersonResource> getChildren() {
6185
return children;
6286
}

0 commit comments

Comments
 (0)