Skip to content

filter generator: create child object context when writing start object. fixes #890 #891

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

Merged
merged 1 commit into from
Jan 17, 2023
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 @@ -313,6 +313,7 @@ public void writeStartObject() throws IOException

TokenFilter f = _filterContext.checkValue(_itemFilter);
if (f == null) {
_filterContext = _filterContext.createChildObjectContext(null, false);
return;
}

Expand Down Expand Up @@ -347,6 +348,7 @@ public void writeStartObject(Object forValue) throws IOException

TokenFilter f = _filterContext.checkValue(_itemFilter);
if (f == null) {
_filterContext = _filterContext.createChildObjectContext(null, false);
return;
}

Expand Down Expand Up @@ -381,6 +383,7 @@ public void writeStartObject(Object forValue, int size) throws IOException

TokenFilter f = _filterContext.checkValue(_itemFilter);
if (f == null) {
_filterContext = _filterContext.createChildObjectContext(null, false);
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package com.fasterxml.jackson.core.filter;

import com.fasterxml.jackson.core.BaseTest;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.filter.TokenFilter.Inclusion;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

// for [core#890]
public class GeneratorFiltering890Test
extends BaseTest
{
private static final class OrTokenFilter extends TokenFilter {

private final List<? extends TokenFilter> delegates;

private OrTokenFilter(final List<? extends TokenFilter> delegates) {
this.delegates = delegates;
}

static OrTokenFilter create(final Set<String> jsonPointers) {
return new OrTokenFilter(jsonPointers.stream().map(JsonPointerBasedFilter::new).collect(Collectors.toList()));
}

@Override
public TokenFilter includeElement(final int index) {
return executeDelegates(delegate -> delegate.includeElement(index));
}

@Override
public TokenFilter includeProperty(final String name) {
return executeDelegates(delegate -> delegate.includeProperty(name));
}

@Override
public TokenFilter filterStartArray() {
return this;
}

@Override
public TokenFilter filterStartObject() {
return this;
}

private TokenFilter executeDelegates(final UnaryOperator<TokenFilter> operator) {
List<TokenFilter> nextDelegates = null;
for (final TokenFilter delegate : delegates) {
final TokenFilter next = operator.apply(delegate);
if (null == next) {
continue;
}
if (TokenFilter.INCLUDE_ALL == next) {
return TokenFilter.INCLUDE_ALL;
}

if (null == nextDelegates) {
nextDelegates = new ArrayList<>(delegates.size());
}
nextDelegates.add(next);
}
return null == nextDelegates ? null : new OrTokenFilter(nextDelegates);
}
}

public void testIssue809_singleProperty() throws Exception
{
// GIVEN
final Set<String> jsonPointers = Stream.of("/0/id").collect(Collectors.toSet());

// WHEN
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JsonGenerator g = new FilteringGeneratorDelegate(createGenerator(outputStream), OrTokenFilter.create(jsonPointers), Inclusion.INCLUDE_ALL_AND_PATH, true);

g.writeStartArray();
writeOuterObject(g, 1, "first", "a", "second", "b");
writeOuterObject(g, 2, "third", "c", "fourth", "d");
g.writeEndArray();
g.flush();
g.close();
outputStream.close();

// THEN
String json = outputStream.toString("US-ASCII");
assertEquals("[{\"id\":1}]", json);
}

public void testIssue809_twoProperties() throws Exception
{
// GIVEN
final Set<String> jsonPointers = Stream.of("/0/id", "/0/stuff/0/name").collect(Collectors.toSet());

// WHEN
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JsonGenerator g = new FilteringGeneratorDelegate(createGenerator(outputStream), OrTokenFilter.create(jsonPointers), Inclusion.INCLUDE_ALL_AND_PATH, true);

g.writeStartArray();
writeOuterObject(g, 1, "first", "a", "second", "b");
writeOuterObject(g, 2, "third", "c", "fourth", "d");
g.writeEndArray();
g.flush();
g.close();
outputStream.close();

// THEN
String json = outputStream.toString("US-ASCII");
assertEquals("[{\"id\":1,\"stuff\":[{\"name\":\"first\"}]}]", json);
}

private static void writeOuterObject(final JsonGenerator g, final int id, final String name1, final String type1, final String name2, final String type2) throws IOException
{
g.writeStartObject();
g.writeFieldName("id");
g.writeNumber(id);
g.writeFieldName("stuff");
g.writeStartArray();
writeInnerObject(g, name1, type1);
writeInnerObject(g, name2, type2);
g.writeEndArray();
g.writeEndObject();
}

private static void writeInnerObject(final JsonGenerator g, final String name, final String type) throws IOException
{
g.writeStartObject();
g.writeFieldName("name");
g.writeString(name);
g.writeFieldName("type");
g.writeString(type);
g.writeEndObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,90 +14,109 @@ public class JsonPointerGeneratorFilteringTest extends com.fasterxml.jackson.cor

public void testSimplePropertyWithPath() throws Exception
{
_assert(SIMPLE_INPUT, "/c", Inclusion.INCLUDE_ALL_AND_PATH, "{'c':{'d':{'a':true}}}");
_assert(SIMPLE_INPUT, "/c/d", Inclusion.INCLUDE_ALL_AND_PATH, "{'c':{'d':{'a':true}}}");
_assert(SIMPLE_INPUT, "/c/d/a", Inclusion.INCLUDE_ALL_AND_PATH, "{'c':{'d':{'a':true}}}");
_assert(SIMPLE_INPUT, "/c", Inclusion.INCLUDE_ALL_AND_PATH, "{'c':{'d':{'a':true}}}", false);
_assert(SIMPLE_INPUT, "/c/d", Inclusion.INCLUDE_ALL_AND_PATH, "{'c':{'d':{'a':true}}}", false);
_assert(SIMPLE_INPUT, "/c/d/a", Inclusion.INCLUDE_ALL_AND_PATH, "{'c':{'d':{'a':true}}}", false);

_assert(SIMPLE_INPUT, "/c/d/a", Inclusion.INCLUDE_ALL_AND_PATH, "{'c':{'d':{'a':true}}}");
_assert(SIMPLE_INPUT, "/c/d/a", Inclusion.INCLUDE_ALL_AND_PATH, "{'c':{'d':{'a':true}}}", false);

_assert(SIMPLE_INPUT, "/a", Inclusion.INCLUDE_ALL_AND_PATH, "{'a':1}");
_assert(SIMPLE_INPUT, "/d", Inclusion.INCLUDE_ALL_AND_PATH, "{'d':null}");
_assert(SIMPLE_INPUT, "/a", Inclusion.INCLUDE_ALL_AND_PATH, "{'a':1}", false);
_assert(SIMPLE_INPUT, "/d", Inclusion.INCLUDE_ALL_AND_PATH, "{'d':null}", false);

// and then non-match
_assert(SIMPLE_INPUT, "/x", Inclusion.INCLUDE_ALL_AND_PATH, "");
_assert(SIMPLE_INPUT, "/x", Inclusion.INCLUDE_ALL_AND_PATH, "", false);
}

public void testSimplePropertyWithoutPath() throws Exception
{
_assert(SIMPLE_INPUT, "/c", Inclusion.ONLY_INCLUDE_ALL, "{'d':{'a':true}}");
_assert(SIMPLE_INPUT, "/c/d", Inclusion.ONLY_INCLUDE_ALL, "{'a':true}");
_assert(SIMPLE_INPUT, "/c/d/a", Inclusion.ONLY_INCLUDE_ALL, "true");
_assert(SIMPLE_INPUT, "/c", Inclusion.ONLY_INCLUDE_ALL, "{'d':{'a':true}}", false);
_assert(SIMPLE_INPUT, "/c/d", Inclusion.ONLY_INCLUDE_ALL, "{'a':true}", false);
_assert(SIMPLE_INPUT, "/c/d/a", Inclusion.ONLY_INCLUDE_ALL, "true", false);

_assert(SIMPLE_INPUT, "/a", Inclusion.ONLY_INCLUDE_ALL, "1");
_assert(SIMPLE_INPUT, "/d", Inclusion.ONLY_INCLUDE_ALL, "null");
_assert(SIMPLE_INPUT, "/a", Inclusion.ONLY_INCLUDE_ALL, "1", false);
_assert(SIMPLE_INPUT, "/d", Inclusion.ONLY_INCLUDE_ALL, "null", false);

// and then non-match
_assert(SIMPLE_INPUT, "/x", Inclusion.ONLY_INCLUDE_ALL, "");
_assert(SIMPLE_INPUT, "/x", Inclusion.ONLY_INCLUDE_ALL, "", false);
}

public void testArrayElementWithPath() throws Exception
{
_assert(SIMPLE_INPUT, "/b", Inclusion.INCLUDE_ALL_AND_PATH, "{'b':[1,2,3]}");
_assert(SIMPLE_INPUT, "/b/1", Inclusion.INCLUDE_ALL_AND_PATH, "{'b':[2]}");
_assert(SIMPLE_INPUT, "/b/2", Inclusion.INCLUDE_ALL_AND_PATH, "{'b':[3]}");
_assert(SIMPLE_INPUT, "/b", Inclusion.INCLUDE_ALL_AND_PATH, "{'b':[1,2,3]}", false);
_assert(SIMPLE_INPUT, "/b/1", Inclusion.INCLUDE_ALL_AND_PATH, "{'b':[2]}", false);
_assert(SIMPLE_INPUT, "/b/2", Inclusion.INCLUDE_ALL_AND_PATH, "{'b':[3]}", false);

// and then non-match
_assert(SIMPLE_INPUT, "/b/8", Inclusion.INCLUDE_ALL_AND_PATH, "");
_assert(SIMPLE_INPUT, "/b/8", Inclusion.INCLUDE_ALL_AND_PATH, "", false);
}

public void testArrayNestedWithPath() throws Exception
{
_assert("{'a':[true,{'b':3,'d':2},false]}", "/a/1/b", Inclusion.INCLUDE_ALL_AND_PATH, "{'a':[{'b':3}]}");
_assert("[true,[1]]", "/0", Inclusion.INCLUDE_ALL_AND_PATH, "[true]");
_assert("[true,[1]]", "/1", Inclusion.INCLUDE_ALL_AND_PATH, "[[1]]");
_assert("[true,[1,2,[true],3],0]", "/0", Inclusion.INCLUDE_ALL_AND_PATH, "[true]");
_assert("[true,[1,2,[true],3],0]", "/1", Inclusion.INCLUDE_ALL_AND_PATH, "[[1,2,[true],3]]");

_assert("[true,[1,2,[true],3],0]", "/1/2", Inclusion.INCLUDE_ALL_AND_PATH, "[[[true]]]");
_assert("[true,[1,2,[true],3],0]", "/1/2/0", Inclusion.INCLUDE_ALL_AND_PATH, "[[[true]]]");
_assert("[true,[1,2,[true],3],0]", "/1/3/0", Inclusion.INCLUDE_ALL_AND_PATH, "");
_assert("{'a':[true,{'b':3,'d':2},false]}", "/a/1/b", Inclusion.INCLUDE_ALL_AND_PATH, "{'a':[{'b':3}]}", false);
_assert("[true,[1]]", "/0", Inclusion.INCLUDE_ALL_AND_PATH, "[true]", false);
_assert("[true,[1]]", "/1", Inclusion.INCLUDE_ALL_AND_PATH, "[[1]]", false);
_assert("[true,[1,2,[true],3],0]", "/0", Inclusion.INCLUDE_ALL_AND_PATH, "[true]", false);
_assert("[true,[1,2,[true],3],0]", "/1", Inclusion.INCLUDE_ALL_AND_PATH, "[[1,2,[true],3]]", false);

_assert("[true,[1,2,[true],3],0]", "/1/2", Inclusion.INCLUDE_ALL_AND_PATH, "[[[true]]]", false);
_assert("[true,[1,2,[true],3],0]", "/1/2/0", Inclusion.INCLUDE_ALL_AND_PATH, "[[[true]]]", false);
_assert("[true,[1,2,[true],3],0]", "/1/3/0", Inclusion.INCLUDE_ALL_AND_PATH, "", false);
}

public void testArrayNestedWithoutPath() throws Exception
{
_assert("{'a':[true,{'b':3,'d':2},false]}", "/a/1/b", Inclusion.ONLY_INCLUDE_ALL, "3");
_assert("[true,[1,2,[true],3],0]", "/0", Inclusion.ONLY_INCLUDE_ALL, "true");
_assert("{'a':[true,{'b':3,'d':2},false]}", "/a/1/b", Inclusion.ONLY_INCLUDE_ALL, "3", false);
_assert("[true,[1,2,[true],3],0]", "/0", Inclusion.ONLY_INCLUDE_ALL, "true", false);
_assert("[true,[1,2,[true],3],0]", "/1", Inclusion.ONLY_INCLUDE_ALL,
"[1,2,[true],3]");
"[1,2,[true],3]", false);

_assert("[true,[1,2,[true],3],0]", "/1/2", Inclusion.ONLY_INCLUDE_ALL, "[true]");
_assert("[true,[1,2,[true],3],0]", "/1/2/0", Inclusion.ONLY_INCLUDE_ALL, "true");
_assert("[true,[1,2,[true],3],0]", "/1/3/0", Inclusion.ONLY_INCLUDE_ALL, "");
_assert("[true,[1,2,[true],3],0]", "/1/2", Inclusion.ONLY_INCLUDE_ALL, "[true]", false);
_assert("[true,[1,2,[true],3],0]", "/1/2/0", Inclusion.ONLY_INCLUDE_ALL, "true", false);
_assert("[true,[1,2,[true],3],0]", "/1/3/0", Inclusion.ONLY_INCLUDE_ALL, "", false);
}

// final String SIMPLE_INPUT = aposToQuotes("{'a':1,'b':[1,2,3],'c':{'d':{'a':true}},'d':null}");

public void testArrayElementWithoutPath() throws Exception
{
_assert(SIMPLE_INPUT, "/b", Inclusion.ONLY_INCLUDE_ALL, "[1,2,3]");
_assert(SIMPLE_INPUT, "/b/1", Inclusion.ONLY_INCLUDE_ALL, "2");
_assert(SIMPLE_INPUT, "/b/2", Inclusion.ONLY_INCLUDE_ALL, "3");
_assert(SIMPLE_INPUT, "/b", Inclusion.ONLY_INCLUDE_ALL, "[1,2,3]", false);
_assert(SIMPLE_INPUT, "/b/1", Inclusion.ONLY_INCLUDE_ALL, "2", false);
_assert(SIMPLE_INPUT, "/b/2", Inclusion.ONLY_INCLUDE_ALL, "3", false);

_assert(SIMPLE_INPUT, "/b/8", Inclusion.ONLY_INCLUDE_ALL, "");
_assert(SIMPLE_INPUT, "/b/8", Inclusion.ONLY_INCLUDE_ALL, "", false);

// and then non-match
_assert(SIMPLE_INPUT, "/x", Inclusion.ONLY_INCLUDE_ALL, "");
_assert(SIMPLE_INPUT, "/x", Inclusion.ONLY_INCLUDE_ALL, "", false);
}

private void _assert(String input, String pathExpr, Inclusion tokenFilterInclusion, String exp)
public void testAllowMultipleMatchesWithPath() throws Exception
{
_assert("[1,2,3]", "/0", Inclusion.INCLUDE_ALL_AND_PATH, "[1]", true);
_assert("[1,2,3]", "/1", Inclusion.INCLUDE_ALL_AND_PATH, "[2]", true);
_assert("[1,2,3]", "/2", Inclusion.INCLUDE_ALL_AND_PATH, "[3]", true);

_assert("{'a':[1,2,3]}", "/a/0", Inclusion.INCLUDE_ALL_AND_PATH, "{'a':[1]}", true);
_assert("{'a':[1,2,3]}", "/a/1", Inclusion.INCLUDE_ALL_AND_PATH, "{'a':[2]}", true);
_assert("{'a':[1,2,3]}", "/a/2", Inclusion.INCLUDE_ALL_AND_PATH, "{'a':[3]}", true);

_assert("[{'id':1},{'id':2},{'id':3}]", "/0/id", Inclusion.INCLUDE_ALL_AND_PATH, "[{'id':1}]", true);
_assert("[{'id':1},{'id':2},{'id':3}]", "/1/id", Inclusion.INCLUDE_ALL_AND_PATH, "[{'id':2}]", true);
_assert("[{'id':1},{'id':2},{'id':3}]", "/2/id", Inclusion.INCLUDE_ALL_AND_PATH, "[{'id':3}]", true);

_assert("[{'id':1,'stuff':[1,2,3]},{'id':2,'stuff':[4,5,6]},{'id':3,'stuff':[7,8,9]}]", "/0/stuff/0", Inclusion.INCLUDE_ALL_AND_PATH, "[{'stuff':[1]}]", true);
_assert("[{'id':1,'stuff':[1,2,3]},{'id':2,'stuff':[4,5,6]},{'id':3,'stuff':[7,8,9]}]", "/1/stuff/1", Inclusion.INCLUDE_ALL_AND_PATH, "[{'stuff':[5]}]", true);
_assert("[{'id':1,'stuff':[1,2,3]},{'id':2,'stuff':[4,5,6]},{'id':3,'stuff':[7,8,9]}]", "/2/stuff/2", Inclusion.INCLUDE_ALL_AND_PATH, "[{'stuff':[9]}]", true);
}

private void _assert(String input, String pathExpr, Inclusion tokenFilterInclusion, String exp, boolean allowMultipleMatches)
throws Exception
{
StringWriter w = new StringWriter();

JsonGenerator g0 = JSON_F.createGenerator(w);
FilteringGeneratorDelegate g = new FilteringGeneratorDelegate(g0,
new JsonPointerBasedFilter(pathExpr),
tokenFilterInclusion, false);
tokenFilterInclusion, allowMultipleMatches);

try {
writeJsonDoc(JSON_F, input, g);
Expand Down