Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jackson-based body filter for JSON #512

Merged
merged 3 commits into from
Jun 13, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.zalando.logbook.json;

import java.io.StringWriter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.zalando.logbook.BodyFilter;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.extern.slf4j.Slf4j;

/**
*
* Thread-safe filter for JSON fields. Filters on property names.
* <br><br>
* Output is always compacted, even in case of invalid JSON,
* so this filter should not be used in conjunction with {@linkplain JsonCompactor}.
*
*/

@Slf4j
public class JacksonJsonFieldBodyFilter implements BodyFilter {

private final static StringReplaceJsonCompactor fallbackCompactor = new StringReplaceJsonCompactor();

private final String replacement;
private final Set<String> fields;
private final ObjectMapper objectMapper;

public JacksonJsonFieldBodyFilter(Collection<String> fieldNames, String replacement, ObjectMapper objectMapper) {
this.fields = new HashSet<>(fieldNames); // thread safe for reading
this.replacement = replacement;
this.objectMapper = objectMapper;
}

public JacksonJsonFieldBodyFilter(Collection<String> fieldNames, String replacement) {
this(fieldNames, replacement, new ObjectMapper());
}

@Override
public String filter(String contentType, String body) {
return JsonMediaType.JSON.test(contentType) ? filter(body) : body;
}

public String filter(final String body) {
try {
JsonFactory factory = objectMapper.getFactory();
final JsonParser parser = factory.createParser(body);

StringWriter writer = new StringWriter(body.length() * 2); // rough estimate of final size

JsonGenerator generator = factory.createGenerator(writer);
try {
while(true) {
JsonToken nextToken = parser.nextToken();
if(nextToken == null) {
break;
}

generator.copyCurrentEvent(parser);
if(nextToken == JsonToken.FIELD_NAME && fields.contains(parser.getCurrentName())) {
nextToken = parser.nextToken();
generator.writeString(replacement);
if(!nextToken.isScalarValue()) {
parser.skipChildren(); // skip children
}
}
}
} finally {
parser.close();

generator.close();
}

return writer.toString();
} catch(Exception e) {
log.trace("Unable to filter body for fields {}, compacting result. `{}`", fields, e.getMessage());
return fallbackCompactor.compact(body);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.zalando.logbook.json;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;

import org.junit.jupiter.api.Test;


public class JacksonJsonFieldBodyFilterTest {

@Test
public void testFilterString() throws Exception {
String filtered = getFilter("email") .filter(getResource("/user.json"));
assertThat(filtered, not(containsString("@entur.org")));
}

@Test
public void testFilterNumber() throws Exception {
String filtered = getFilter("id") .filter(getResource("/user.json"));
assertThat(filtered, not(containsString("18375")));
}

@Test
public void testFilterObject() throws Exception {
String filtered = getFilter("cars") .filter(getResource("/cars-object.json"));
assertThat(filtered, not(containsString("Ford")));
}

@Test
public void testFilterArray() throws Exception {
String filtered = getFilter("cars") .filter(getResource("/cars-array.json"));
assertThat(filtered, not(containsString("Ford")));
}

@Test
public void testFilterHugeBodyObject1() throws Exception {
String string = getResource("/huge-sample.json");
String filtered = getFilter("name").filter(string);
assertThat(filtered, not(containsString("Pena Hudson")));
assertThat(filtered.length(), is(lessThan(string.length())));
}

@Test
public void testFilterHugeBodyObject2() throws Exception {
Set<String> remove = new HashSet<>();
remove.add("name");
String string = getResource("/huge-sample.json");
String filtered = getFilter(remove).filter("application/json", string);
assertThat(filtered, not(containsString("Pena Hudson")));
assertThat(filtered.length(), is(lessThan(string.length())));
}

@Test
public void doesNotFilterInvalidJson() throws Exception {
String valid = getResource("/cars-array.json").trim();
String invalid = valid.substring(0, valid.length() - 1);
String filtered = getFilter("cars").filter(invalid);
assertThat(filtered, containsString("Ford"));
}

@Test
public void doesNotFilterNonJson() throws Exception {
String valid = getResource("/cars-array.json").trim();
String invalid = valid.substring(0, valid.length() - 1);
String filtered = getFilter("cars").filter("application/xml", invalid);
assertThat(filtered, containsString("Ford"));
}

private String getResource(String path) throws IOException {
final byte[] bytes = Files.readAllBytes(Paths.get("src/test/resources/" + path));
return new String(bytes, UTF_8);
}

public static JacksonJsonFieldBodyFilter getFilter(String ... fieldNames) {
return new JacksonJsonFieldBodyFilter(Arrays.asList(fieldNames), "XXX");
}

public static JacksonJsonFieldBodyFilter getFilter(Collection<String> fieldNames) {
return new JacksonJsonFieldBodyFilter(fieldNames , "XXX");
}

}
9 changes: 9 additions & 0 deletions logbook-json/src/test/resources/cars-array.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name":"John",
"age":30,
"cars": [
{"car1":"Ford"},
{"car2":"BMW"},
{"car3":"Fiat"}
]
}
9 changes: 9 additions & 0 deletions logbook-json/src/test/resources/cars-object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name":"John",
"age":30,
"cars": {
"car1":"Ford",
"car2":"BMW",
"car3":"Fiat"
}
}
11 changes: 11 additions & 0 deletions logbook-json/src/test/resources/user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": 1,
"customerNumber": 1234567,
"profileType": "S",
"status": "A",
"firstName": "My",
"surname": "Name",
"email": "someone@zalando.org",
"languagePreference": "NO",
"uuid": "658DE1D4BB4F3AC4E053020011AC1EC3"
}