|  | @@ -0,0 +1,549 @@
 | 
	
		
			
				|  |  | +/*
 | 
	
		
			
				|  |  | + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 | 
	
		
			
				|  |  | + * or more contributor license agreements. Licensed under the Elastic License
 | 
	
		
			
				|  |  | + * 2.0 and the Server Side Public License, v 1; you may not use this file except
 | 
	
		
			
				|  |  | + * in compliance with, at your election, the Elastic License 2.0 or the Server
 | 
	
		
			
				|  |  | + * Side Public License, v 1.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +package org.elasticsearch.common.xcontent.support;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import org.elasticsearch.common.Strings;
 | 
	
		
			
				|  |  | +import org.elasticsearch.common.bytes.BytesArray;
 | 
	
		
			
				|  |  | +import org.elasticsearch.common.bytes.BytesReference;
 | 
	
		
			
				|  |  | +import org.elasticsearch.common.io.stream.BytesStreamOutput;
 | 
	
		
			
				|  |  | +import org.elasticsearch.common.xcontent.XContentFieldFilter;
 | 
	
		
			
				|  |  | +import org.elasticsearch.common.xcontent.XContentHelper;
 | 
	
		
			
				|  |  | +import org.elasticsearch.core.CheckedFunction;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xcontent.ToXContentObject;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xcontent.XContentBuilder;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xcontent.XContentFactory;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xcontent.XContentParser;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xcontent.XContentParserConfiguration;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xcontent.XContentType;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import java.io.IOException;
 | 
	
		
			
				|  |  | +import java.util.Collections;
 | 
	
		
			
				|  |  | +import java.util.Map;
 | 
	
		
			
				|  |  | +import java.util.Set;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import static java.util.Collections.emptySet;
 | 
	
		
			
				|  |  | +import static java.util.Collections.singleton;
 | 
	
		
			
				|  |  | +import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap;
 | 
	
		
			
				|  |  | +import static org.elasticsearch.common.xcontent.XContentHelper.toXContent;
 | 
	
		
			
				|  |  | +import static org.elasticsearch.test.MapMatcher.assertMap;
 | 
	
		
			
				|  |  | +import static org.elasticsearch.test.MapMatcher.matchesMap;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +public class XContentFieldFilterTests extends AbstractFilteringTestCase {
 | 
	
		
			
				|  |  | +    @Override
 | 
	
		
			
				|  |  | +    protected void testFilter(Builder expected, Builder actual, Set<String> includes, Set<String> excludes) throws IOException {
 | 
	
		
			
				|  |  | +        final XContentType xContentType = randomFrom(XContentType.values());
 | 
	
		
			
				|  |  | +        final boolean humanReadable = randomBoolean();
 | 
	
		
			
				|  |  | +        String[] sourceIncludes;
 | 
	
		
			
				|  |  | +        if (includes == null) {
 | 
	
		
			
				|  |  | +            sourceIncludes = randomBoolean() ? Strings.EMPTY_ARRAY : null;
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +            sourceIncludes = includes.toArray(new String[includes.size()]);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        String[] sourceExcludes;
 | 
	
		
			
				|  |  | +        if (excludes == null) {
 | 
	
		
			
				|  |  | +            sourceExcludes = randomBoolean() ? Strings.EMPTY_ARRAY : null;
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +            sourceExcludes = excludes.toArray(new String[excludes.size()]);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        XContentFieldFilter filter = XContentFieldFilter.newFieldFilter(sourceIncludes, sourceExcludes);
 | 
	
		
			
				|  |  | +        BytesReference ref = filter.apply(toBytesReference(actual, xContentType, humanReadable), xContentType);
 | 
	
		
			
				|  |  | +        assertMap(XContentHelper.convertToMap(ref, true, xContentType).v2(), matchesMap(toMap(expected, xContentType, humanReadable)));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private void testFilter(String expectedJson, String actualJson, Set<String> includes, Set<String> excludes) throws IOException {
 | 
	
		
			
				|  |  | +        CheckedFunction<String, Builder, IOException> toBuilder = json -> {
 | 
	
		
			
				|  |  | +            XContentParser parser = XContentHelper.createParser(XContentParserConfiguration.EMPTY, new BytesArray(json), XContentType.JSON);
 | 
	
		
			
				|  |  | +            if ((parser.currentToken() == null) && (parser.nextToken() == null)) {
 | 
	
		
			
				|  |  | +                return builder -> builder;
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            return builder -> builder.copyCurrentStructure(parser);
 | 
	
		
			
				|  |  | +        };
 | 
	
		
			
				|  |  | +        testFilter(toBuilder.apply(expectedJson), toBuilder.apply(actualJson), includes, excludes);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testPrefixedNamesFilteringTest() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": "value",
 | 
	
		
			
				|  |  | +                "obj_name": "value_name"
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj_name": "value_name"
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("obj_name"), emptySet());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testNestedFiltering() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "array": [{
 | 
	
		
			
				|  |  | +                    "nested": 2,
 | 
	
		
			
				|  |  | +                    "nested_2": 3
 | 
	
		
			
				|  |  | +                }]
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "array": [{
 | 
	
		
			
				|  |  | +                    "nested": 2
 | 
	
		
			
				|  |  | +                }]
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("array.nested"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "array": [{
 | 
	
		
			
				|  |  | +                    "nested": 2,
 | 
	
		
			
				|  |  | +                    "nested_2": 3
 | 
	
		
			
				|  |  | +                }]
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("array.*"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value",
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("obj.field"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value",
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("obj.*"), emptySet());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testCompleteObjectFiltering() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value",
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                },
 | 
	
		
			
				|  |  | +                "array": [{
 | 
	
		
			
				|  |  | +                    "field": "value",
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                }]
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value",
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("obj"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("obj"), singleton("*.field2"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "array": [{
 | 
	
		
			
				|  |  | +                    "field": "value",
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                }]
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("array"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "array": [{
 | 
	
		
			
				|  |  | +                    "field": "value"
 | 
	
		
			
				|  |  | +                }]
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("array"), singleton("*.field2"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testFilterIncludesUsingStarPrefix() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value",
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                },
 | 
	
		
			
				|  |  | +                "n_obj": {
 | 
	
		
			
				|  |  | +                    "n_field": "value",
 | 
	
		
			
				|  |  | +                    "n_field2": "value2"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("*.field2"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // only objects
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value",
 | 
	
		
			
				|  |  | +                    "field2": "value2"
 | 
	
		
			
				|  |  | +                },
 | 
	
		
			
				|  |  | +                "n_obj": {
 | 
	
		
			
				|  |  | +                    "n_field": "value",
 | 
	
		
			
				|  |  | +                    "n_field2": "value2"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("*.*"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "field": "value"
 | 
	
		
			
				|  |  | +                },
 | 
	
		
			
				|  |  | +                "n_obj": {
 | 
	
		
			
				|  |  | +                    "n_field": "value"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("*"), singleton("*.*2"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testFilterWithEmptyIncludesExcludes() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value"
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(actual, actual, emptySet(), emptySet());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testThatFilterIncludesEmptyObjectWhenUsingIncludes() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {}
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(actual, actual, singleton("obj"), emptySet());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testThatFilterIncludesEmptyObjectWhenUsingExcludes() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {}
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(actual, actual, emptySet(), singleton("nonExistingField"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // wait for PR https://github.com/FasterXML/jackson-core/pull/729 to be introduced
 | 
	
		
			
				|  |  | +    @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/80160")
 | 
	
		
			
				|  |  | +    public void testNotOmittingObjectsWithExcludedProperties() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {
 | 
	
		
			
				|  |  | +                    "f1": "f2"
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj": {}
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, emptySet(), singleton("obj.f1"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testNotOmittingObjectWithNestedExcludedObject() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj1": {
 | 
	
		
			
				|  |  | +                    "obj2": {
 | 
	
		
			
				|  |  | +                        "obj3": {}
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj1": {}
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        // implicit include
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, emptySet(), singleton("*.obj2"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // explicit include
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("obj1"), singleton("*.obj2"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // wildcard include
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj1": {
 | 
	
		
			
				|  |  | +                    "obj2": {}
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("*.obj2"), singleton("*.obj3"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testIncludingObjectWithNestedIncludedObject() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "obj1": {
 | 
	
		
			
				|  |  | +                    "obj2": {}
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(actual, actual, singleton("*.obj2"), emptySet());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testDotsInFieldNames() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "foo.bar": 2,
 | 
	
		
			
				|  |  | +                "foo": {
 | 
	
		
			
				|  |  | +                    "baz": 3
 | 
	
		
			
				|  |  | +                },
 | 
	
		
			
				|  |  | +                "quux": 5
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "foo.bar": 2,
 | 
	
		
			
				|  |  | +                "foo": {
 | 
	
		
			
				|  |  | +                    "baz": 3
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("foo"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // dots in field names in excludes
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "quux": 5
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, emptySet(), singleton("foo"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +    * Tests that we can extract paths containing non-ascii characters.
 | 
	
		
			
				|  |  | +    * See {@link AbstractFilteringTestCase#testFilterSupplementaryCharactersInPaths()}
 | 
	
		
			
				|  |  | +    * for a similar test but for XContent.
 | 
	
		
			
				|  |  | +    */
 | 
	
		
			
				|  |  | +    public void testSupplementaryCharactersInPaths() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "搜索": 2,
 | 
	
		
			
				|  |  | +                "指数": 3
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "搜索": 2
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("搜索"), emptySet());
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "指数": 3
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, emptySet(), singleton("搜索"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +    * Tests that we can extract paths which share a prefix with other paths.
 | 
	
		
			
				|  |  | +    * See {@link AbstractFilteringTestCase#testFilterSharedPrefixes()}
 | 
	
		
			
				|  |  | +    * for a similar test but for XContent.
 | 
	
		
			
				|  |  | +    */
 | 
	
		
			
				|  |  | +    public void testSharedPrefixes() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "foobar": 2,
 | 
	
		
			
				|  |  | +                "foobaz": 3
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "foobar": 2
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("foobar"), emptySet());
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "foobaz": 3
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, emptySet(), singleton("foobar"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // wait for PR https://github.com/FasterXML/jackson-core/pull/729 to be introduced
 | 
	
		
			
				|  |  | +    @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/80160")
 | 
	
		
			
				|  |  | +    public void testArraySubFieldExclusion() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "array": [{
 | 
	
		
			
				|  |  | +                    "exclude": "bar"
 | 
	
		
			
				|  |  | +                }]
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "array": []
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, emptySet(), singleton("array.exclude"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // wait for PR https://github.com/FasterXML/jackson-core/pull/729 to be introduced
 | 
	
		
			
				|  |  | +    @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/80160")
 | 
	
		
			
				|  |  | +    public void testEmptyArraySubFieldsExclusion() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "array": []
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(actual, actual, emptySet(), singleton("array.exclude"));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testEmptyArraySubFieldsInclusion() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "array": []
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = "{}";
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("array.include"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "array": []
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, Set.of("array", "array.include"), emptySet());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testEmptyObjectsSubFieldsInclusion() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "field": "value",
 | 
	
		
			
				|  |  | +                "object": {}
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = "{}";
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("object.include"), emptySet());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "object": {}
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, Set.of("object", "object.include"), emptySet());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +    * Tests that we can extract paths which have another path as a prefix.
 | 
	
		
			
				|  |  | +    * See {@link AbstractFilteringTestCase#testFilterPrefix()}
 | 
	
		
			
				|  |  | +    * for a similar test but for XContent.
 | 
	
		
			
				|  |  | +    */
 | 
	
		
			
				|  |  | +    public void testPrefix() throws IOException {
 | 
	
		
			
				|  |  | +        String actual = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "photos": ["foo", "bar"],
 | 
	
		
			
				|  |  | +                "photosCount": 2
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        String expected = """
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                "photosCount": 2
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            """;
 | 
	
		
			
				|  |  | +        testFilter(expected, actual, singleton("photosCount"), emptySet());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testEmptySource() throws IOException {
 | 
	
		
			
				|  |  | +        final CheckedFunction<XContentType, BytesReference, IOException> emptyValueSupplier = xContentType -> {
 | 
	
		
			
				|  |  | +            BytesStreamOutput bStream = new BytesStreamOutput();
 | 
	
		
			
				|  |  | +            XContentBuilder builder = XContentFactory.contentBuilder(xContentType, bStream).map(Collections.emptyMap());
 | 
	
		
			
				|  |  | +            builder.close();
 | 
	
		
			
				|  |  | +            return bStream.bytes();
 | 
	
		
			
				|  |  | +        };
 | 
	
		
			
				|  |  | +        final XContentType xContentType = randomFrom(XContentType.values());
 | 
	
		
			
				|  |  | +        // null value for parser filter
 | 
	
		
			
				|  |  | +        assertEquals(
 | 
	
		
			
				|  |  | +            emptyValueSupplier.apply(xContentType),
 | 
	
		
			
				|  |  | +            XContentFieldFilter.newFieldFilter(new String[0], new String[0]).apply(null, xContentType)
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        // empty bytes for parser filter
 | 
	
		
			
				|  |  | +        assertEquals(
 | 
	
		
			
				|  |  | +            emptyValueSupplier.apply(xContentType),
 | 
	
		
			
				|  |  | +            XContentFieldFilter.newFieldFilter(new String[0], new String[0]).apply(BytesArray.EMPTY, xContentType)
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        // null value for map filter
 | 
	
		
			
				|  |  | +        assertEquals(
 | 
	
		
			
				|  |  | +            emptyValueSupplier.apply(xContentType),
 | 
	
		
			
				|  |  | +            XContentFieldFilter.newFieldFilter(new String[0], new String[] { "test*" }).apply(null, xContentType)
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        // empty bytes for map filter
 | 
	
		
			
				|  |  | +        assertEquals(
 | 
	
		
			
				|  |  | +            emptyValueSupplier.apply(xContentType),
 | 
	
		
			
				|  |  | +            XContentFieldFilter.newFieldFilter(new String[0], new String[] { "test*" }).apply(BytesArray.EMPTY, xContentType)
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private BytesReference toBytesReference(Builder builder, XContentType xContentType, boolean humanReadable) throws IOException {
 | 
	
		
			
				|  |  | +        return toXContent((ToXContentObject) (xContentBuilder, params) -> builder.apply(xContentBuilder), xContentType, humanReadable);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private static Map<String, Object> toMap(Builder test, XContentType xContentType, boolean humanReadable) throws IOException {
 | 
	
		
			
				|  |  | +        ToXContentObject toXContent = (builder, params) -> test.apply(builder);
 | 
	
		
			
				|  |  | +        return convertToMap(toXContent(toXContent, xContentType, humanReadable), true, xContentType).v2();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @Override
 | 
	
		
			
				|  |  | +    protected boolean removesEmptyArrays() {
 | 
	
		
			
				|  |  | +        return false;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 |