Skip to content

Commit a7e2965

Browse files
committed
Preserve empty objects and arrays when filtering
This change updates our support for filtering so that empty arrays and objects are preserved when they are present in the content that is filtering. Closes elastic#63842
1 parent 5fe7b7f commit a7e2965

File tree

3 files changed

+133
-4
lines changed

3 files changed

+133
-4
lines changed

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/json/JsonXContentGenerator.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.elasticsearch.common.xcontent.XContentGenerator;
3636
import org.elasticsearch.common.xcontent.XContentParser;
3737
import org.elasticsearch.common.xcontent.XContentType;
38+
import org.elasticsearch.common.xcontent.support.filtering.EmptyPreservingFilteringGeneratorDelegate;
3839
import org.elasticsearch.common.xcontent.support.filtering.FilterPathBasedFilter;
3940
import org.elasticsearch.core.internal.io.Streams;
4041

@@ -86,12 +87,12 @@ public JsonXContentGenerator(JsonGenerator jsonGenerator, OutputStream os, Set<S
8687

8788
boolean hasExcludes = excludes.isEmpty() == false;
8889
if (hasExcludes) {
89-
generator = new FilteringGeneratorDelegate(generator, new FilterPathBasedFilter(excludes, false), true, true);
90+
generator = new EmptyPreservingFilteringGeneratorDelegate(generator, new FilterPathBasedFilter(excludes, false), true, true);
9091
}
9192

9293
boolean hasIncludes = includes.isEmpty() == false;
9394
if (hasIncludes) {
94-
generator = new FilteringGeneratorDelegate(generator, new FilterPathBasedFilter(includes, true), true, true);
95+
generator = new EmptyPreservingFilteringGeneratorDelegate(generator, new FilterPathBasedFilter(includes, true), true, true);
9596
}
9697

9798
if (hasExcludes || hasIncludes) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common.xcontent.support.filtering;
21+
22+
import com.fasterxml.jackson.core.JsonGenerator;
23+
import com.fasterxml.jackson.core.JsonStreamContext;
24+
import com.fasterxml.jackson.core.filter.FilteringGeneratorDelegate;
25+
import com.fasterxml.jackson.core.filter.TokenFilter;
26+
import com.fasterxml.jackson.core.filter.TokenFilterContext;
27+
28+
import java.io.IOException;
29+
30+
/**
31+
* An extension of the Jockson {@link FilteringGeneratorDelegate} that preserves empty objects and
32+
* empty arrays in the filtered output. This is done by checking values during the close of the
33+
* array or object inside a custom token filter context.
34+
*/
35+
public class EmptyPreservingFilteringGeneratorDelegate extends FilteringGeneratorDelegate {
36+
37+
public EmptyPreservingFilteringGeneratorDelegate(JsonGenerator generator,
38+
TokenFilter filter,
39+
boolean includePath,
40+
boolean allowMultipleMatches
41+
) {
42+
super(generator, filter, includePath, allowMultipleMatches);
43+
this._filterContext = new EmptyPreservingTokenFilterContext(EmptyPreservingTokenFilterContext.ROOT, null, filter, true);
44+
}
45+
46+
static class EmptyPreservingTokenFilterContext extends TokenFilterContext {
47+
48+
public static final int ROOT = JsonStreamContext.TYPE_ROOT;
49+
50+
EmptyPreservingTokenFilterContext(int type, TokenFilterContext parent, TokenFilter filter, boolean startHandled) {
51+
super(type, parent, filter, startHandled);
52+
}
53+
54+
@Override
55+
public TokenFilterContext createChildArrayContext(TokenFilter filter, boolean writeStart) {
56+
TokenFilterContext ctxt = _child;
57+
if (ctxt == null) {
58+
_child = ctxt = new EmptyPreservingTokenFilterContext(TYPE_ARRAY, this, filter, writeStart);
59+
return ctxt;
60+
}
61+
return ((EmptyPreservingTokenFilterContext) ctxt).reset(TYPE_ARRAY, filter, writeStart);
62+
}
63+
64+
@Override
65+
public TokenFilterContext createChildObjectContext(TokenFilter filter, boolean writeStart) {
66+
TokenFilterContext ctxt = _child;
67+
if (ctxt == null) {
68+
_child = ctxt = new EmptyPreservingTokenFilterContext(TYPE_OBJECT, this, filter, writeStart);
69+
return ctxt;
70+
}
71+
return ((EmptyPreservingTokenFilterContext) ctxt).reset(TYPE_OBJECT, filter, writeStart);
72+
}
73+
74+
@Override
75+
public TokenFilterContext closeArray(JsonGenerator gen) throws IOException {
76+
if (_startHandled) {
77+
gen.writeEndArray();
78+
}
79+
if ((_filter != null) && (_filter != TokenFilter.INCLUDE_ALL)) {
80+
if (_type == TYPE_ARRAY && _index == -1) { // empty
81+
writePath(gen);
82+
gen.writeEndArray();
83+
}
84+
_filter.filterFinishArray();
85+
}
86+
return _parent;
87+
}
88+
89+
@Override
90+
public TokenFilterContext closeObject(JsonGenerator gen) throws IOException {
91+
if (_startHandled) {
92+
gen.writeEndObject();
93+
}
94+
if ((_filter != null) && (_filter != TokenFilter.INCLUDE_ALL)) {
95+
if (isStartHandled() == false && _child == null && hasCurrentName() == false) {
96+
// empty!
97+
_parent.writePath(gen);
98+
gen.writeStartObject();
99+
gen.writeEndObject();
100+
}
101+
_filter.filterFinishObject();
102+
}
103+
return _parent;
104+
}
105+
}
106+
}

server/src/test/java/org/elasticsearch/common/xcontent/support/filtering/FilterPathGeneratorFilteringTests.java

+24-2
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,32 @@ public void testExclusiveFiltersWithDots() throws Exception {
167167
assertResult("{'a':0,'b.c':'value','b':{'c':'c_value'}}", "b\\.c", false, "{'a':0,'b':{'c':'c_value'}}");
168168
}
169169

170+
public void testWithEmptyArraysAndObjects() throws Exception {
171+
assertResult("{ 'foo': 'bar', 'hits': [] }", "**.not_there", false, "{'foo':'bar','hits':[]}");
172+
assertResult("{ 'foo': 'bar', 'hits': {} }", "**.not_there", false, "{'foo':'bar','hits':{}}");
173+
174+
// only include foo
175+
assertResult("{ 'foo': 'bar', 'hits': [] }", "foo", true, "{'foo':'bar'}");
176+
assertResult("{ 'foo': 'bar', 'hits': {} }", "foo", true, "{'foo':'bar'}");
177+
178+
// exclude the empty values
179+
assertResult("{ 'foo': 'bar', 'hits': [] }", "hits", false, "{'foo':'bar'}");
180+
assertResult("{ 'foo': 'bar', 'hits': {} }", "hits", false, "{'foo':'bar'}");
181+
182+
// empty at non-root level
183+
assertResult("{ 'foo': 'bar', 'a' : { 'hits': [] } }", "**.not_empty", false, "{'foo':'bar','a':{'hits':[]}}");
184+
assertResult("{ 'foo': 'bar', 'a' : { 'hits': {} } }", "**.not_empty", false, "{'foo':'bar','a':{'hits':{}}}");
185+
186+
// empty empty at non-root level
187+
assertResult("{ 'foo': 'bar', 'a' : { 'hits': [] } }", "**.hits", false, "{'foo':'bar'}");
188+
assertResult("{ 'foo': 'bar', 'a' : { 'hits': {} } }", "**.hits", false, "{'foo':'bar'}");
189+
}
190+
170191
private void assertResult(String input, String filter, boolean inclusive, String expected) throws Exception {
171192
try (BytesStreamOutput os = new BytesStreamOutput()) {
172-
try (FilteringGeneratorDelegate generator = new FilteringGeneratorDelegate(JSON_FACTORY.createGenerator(os),
173-
new FilterPathBasedFilter(Collections.singleton(filter), inclusive), true, true)) {
193+
FilterPathBasedFilter pathBasedFilter = new FilterPathBasedFilter(Collections.singleton(filter), inclusive);
194+
try (FilteringGeneratorDelegate generator = new EmptyPreservingFilteringGeneratorDelegate(JSON_FACTORY.createGenerator(os),
195+
pathBasedFilter, true, true)) {
174196
try (JsonParser parser = JSON_FACTORY.createParser(replaceQuotes(input))) {
175197
while (parser.nextToken() != null) {
176198
generator.copyCurrentStructure(parser);

0 commit comments

Comments
 (0)