diff --git a/libs/x-content/impl/build.gradle b/libs/x-content/impl/build.gradle
index a0b3a96b5c107..a6985d9c2986c 100644
--- a/libs/x-content/impl/build.gradle
+++ b/libs/x-content/impl/build.gradle
@@ -15,7 +15,7 @@ String jacksonVersion = "2.14.1"
dependencies {
compileOnly project(':libs:elasticsearch-core')
compileOnly project(':libs:elasticsearch-x-content')
- implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}"
+ implementation project(path: ':libs:x-content:impl:es-jackson-core', configuration: 'shadow')
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:${jacksonVersion}"
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${jacksonVersion}"
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${jacksonVersion}"
diff --git a/libs/x-content/impl/es-jackson-core/build.gradle b/libs/x-content/impl/es-jackson-core/build.gradle
new file mode 100644
index 0000000000000..7e9d8c56c0ad8
--- /dev/null
+++ b/libs/x-content/impl/es-jackson-core/build.gradle
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'elasticsearch.build'
+apply plugin: 'com.github.johnrengelman.shadow'
+
+String jacksonVersion = "2.14.1"
+
+dependencies {
+ implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}"
+}
+
+['jarHell', 'thirdPartyAudit', 'forbiddenApisMain', 'splitPackagesAudit', 'checkstyleMain', 'licenseHeaders', 'spotlessJavaCheck'].each {
+ tasks.named(it).configure {
+ enabled = false
+ }
+}
+
+tasks.named("dependencyLicenses").configure {
+ mapping from: /jackson-.*/, to: 'jackson'
+}
+
+shadowJar {
+ manifest {
+ attributes 'Multi-Release' : 'true'
+ }
+}
diff --git a/libs/x-content/impl/es-jackson-core/licenses/jackson-LICENSE.txt b/libs/x-content/impl/es-jackson-core/licenses/jackson-LICENSE.txt
new file mode 100644
index 0000000000000..f5f45d26a49d6
--- /dev/null
+++ b/libs/x-content/impl/es-jackson-core/licenses/jackson-LICENSE.txt
@@ -0,0 +1,8 @@
+This copy of Jackson JSON processor streaming parser/generator is licensed under the
+Apache (Software) License, version 2.0 ("the License").
+See the License for details about distribution rights, and the
+specific rights regarding derivate works.
+
+You may obtain a copy of the License at:
+
+http://www.apache.org/licenses/LICENSE-2.0
diff --git a/libs/x-content/impl/es-jackson-core/licenses/jackson-NOTICE.txt b/libs/x-content/impl/es-jackson-core/licenses/jackson-NOTICE.txt
new file mode 100644
index 0000000000000..4c976b7b4cc58
--- /dev/null
+++ b/libs/x-content/impl/es-jackson-core/licenses/jackson-NOTICE.txt
@@ -0,0 +1,20 @@
+# Jackson JSON processor
+
+Jackson is a high-performance, Free/Open Source JSON processing library.
+It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
+been in development since 2007.
+It is currently developed by a community of developers, as well as supported
+commercially by FasterXML.com.
+
+## Licensing
+
+Jackson core and extension components may licensed under different licenses.
+To find the details that apply to this artifact see the accompanying LICENSE file.
+For more information, including possible other licensing options, contact
+FasterXML.com (http://fasterxml.com).
+
+## Credits
+
+A list of contributors may be found from CREDITS file, which is included
+in some artifacts (usually source distributions); but is always available
+from the source code management (SCM) system project uses.
diff --git a/libs/x-content/impl/es-jackson-core/src/main/java/com/fasterxml/jackson/core/filter/FilteringParserDelegate.java b/libs/x-content/impl/es-jackson-core/src/main/java/com/fasterxml/jackson/core/filter/FilteringParserDelegate.java
new file mode 100644
index 0000000000000..32e6b06b778d0
--- /dev/null
+++ b/libs/x-content/impl/es-jackson-core/src/main/java/com/fasterxml/jackson/core/filter/FilteringParserDelegate.java
@@ -0,0 +1,1059 @@
+package com.fasterxml.jackson.core.filter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import com.fasterxml.jackson.core.*;
+import com.fasterxml.jackson.core.filter.TokenFilter.Inclusion;
+import com.fasterxml.jackson.core.util.JsonParserDelegate;
+
+import static com.fasterxml.jackson.core.JsonTokenId.*;
+
+/**
+ * Specialized {@link JsonParserDelegate} that allows use of
+ * {@link TokenFilter} for outputting a subset of content that
+ * is visible to caller
+ *
+ * @since 2.6
+ */
+public class FilteringParserDelegate extends JsonParserDelegate
+{
+ /*
+ /**********************************************************
+ /* Configuration
+ /**********************************************************
+ */
+
+ /**
+ * Object consulted to determine whether to write parts of content generator
+ * is asked to write or not.
+ */
+ protected TokenFilter rootFilter;
+
+ /**
+ * Flag that determines whether filtering will continue after the first
+ * match is indicated or not: if `false`, output is based on just the first
+ * full match (returning {@link TokenFilter#INCLUDE_ALL}) and no more
+ * checks are made; if `true` then filtering will be applied as necessary
+ * until end of content.
+ */
+ protected boolean _allowMultipleMatches;
+
+ /**
+ * Flag that determines whether path leading up to included content should
+ * also be automatically included or not. If `false`, no path inclusion is
+ * done and only explicitly included entries are output; if `true` then
+ * path from main level down to match is also included as necessary.
+ */
+ protected TokenFilter.Inclusion _inclusion;
+
+ /*
+ /**********************************************************
+ /* State
+ /**********************************************************
+ */
+
+ /**
+ * Last token retrieved via {@link #nextToken}, if any.
+ * Null before the first call to nextToken()
,
+ * as well as if token has been explicitly cleared
+ */
+ protected JsonToken _currToken;
+
+ /**
+ * Last cleared token, if any: that is, value that was in
+ * effect when {@link #clearCurrentToken} was called.
+ */
+ protected JsonToken _lastClearedToken;
+
+ /**
+ * During traversal this is the actual "open" parse tree, which sometimes
+ * is the same as {@link #_exposedContext}, and at other times is ahead
+ * of it. Note that this context is never null.
+ */
+ protected TokenFilterContext _headContext;
+
+ /**
+ * In cases where {@link #_headContext} is "ahead" of context exposed to
+ * caller, this context points to what is currently exposed to caller.
+ * When the two are in sync, this context reference will be null
.
+ */
+ protected TokenFilterContext _exposedContext;
+
+ /**
+ * State that applies to the item within container, used where applicable.
+ * Specifically used to pass inclusion state between property name and
+ * property, and also used for array elements.
+ */
+ protected TokenFilter _itemFilter;
+
+ /**
+ * Number of tokens for which {@link TokenFilter#INCLUDE_ALL}
+ * has been returned.
+ */
+ protected int _matchCount;
+
+ /*
+ /**********************************************************
+ /* Construction, initialization
+ /**********************************************************
+ */
+
+ @Deprecated
+ public FilteringParserDelegate(JsonParser p, TokenFilter f,
+ boolean includePath, boolean allowMultipleMatches)
+ {
+ this(p, f, includePath ? Inclusion.INCLUDE_ALL_AND_PATH : Inclusion.ONLY_INCLUDE_ALL, allowMultipleMatches);
+ }
+
+ /**
+ * @param p Parser to delegate calls to
+ * @param f Filter to use
+ * @param inclusion Definition of inclusion criteria
+ * @param allowMultipleMatches Whether to allow multiple matches
+ */
+ public FilteringParserDelegate(JsonParser p, TokenFilter f,
+ TokenFilter.Inclusion inclusion, boolean allowMultipleMatches)
+ {
+ super(p);
+ rootFilter = f;
+ // and this is the currently active filter for root values
+ _itemFilter = f;
+ _headContext = TokenFilterContext.createRootContext(f);
+ _inclusion = inclusion;
+ _allowMultipleMatches = allowMultipleMatches;
+ }
+
+ /*
+ /**********************************************************
+ /* Extended API
+ /**********************************************************
+ */
+
+ public TokenFilter getFilter() { return rootFilter; }
+
+ /**
+ * Accessor for finding number of matches, where specific token and sub-tree
+ * starting (if structured type) are passed.
+ *
+ * @return Number of matches
+ */
+ public int getMatchCount() {
+ return _matchCount;
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, token accessors
+ /**********************************************************
+ */
+
+ @Override public JsonToken getCurrentToken() { return _currToken; }
+ @Override public JsonToken currentToken() { return _currToken; }
+
+ @Deprecated // since 2.12
+ @Override public final int getCurrentTokenId() {
+ return currentTokenId();
+ }
+ @Override public final int currentTokenId() {
+ final JsonToken t = _currToken;
+ return (t == null) ? JsonTokenId.ID_NO_TOKEN : t.id();
+ }
+
+ @Override public boolean hasCurrentToken() { return _currToken != null; }
+ @Override public boolean hasTokenId(int id) {
+ final JsonToken t = _currToken;
+ if (t == null) {
+ return (JsonTokenId.ID_NO_TOKEN == id);
+ }
+ return t.id() == id;
+ }
+
+ @Override public final boolean hasToken(JsonToken t) {
+ return (_currToken == t);
+ }
+
+ @Override public boolean isExpectedStartArrayToken() { return _currToken == JsonToken.START_ARRAY; }
+ @Override public boolean isExpectedStartObjectToken() { return _currToken == JsonToken.START_OBJECT; }
+
+ @Override public JsonLocation getCurrentLocation() { return delegate.getCurrentLocation(); }
+
+ @Override
+ public JsonStreamContext getParsingContext() {
+ return _filterContext();
+ }
+
+ // !!! TODO: Verify it works as expected: copied from standard JSON parser impl
+ @Override
+ public String getCurrentName() throws IOException {
+ JsonStreamContext ctxt = _filterContext();
+ if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) {
+ JsonStreamContext parent = ctxt.getParent();
+ return (parent == null) ? null : parent.getCurrentName();
+ }
+ return ctxt.getCurrentName();
+ }
+
+ // 2.13: IMPORTANT! Must override along with older getCurrentName()
+ @Override
+ public String currentName() throws IOException {
+ JsonStreamContext ctxt = _filterContext();
+ if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) {
+ JsonStreamContext parent = ctxt.getParent();
+ return (parent == null) ? null : parent.getCurrentName();
+ }
+ return ctxt.getCurrentName();
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, token state overrides
+ /**********************************************************
+ */
+
+ @Override
+ public void clearCurrentToken() {
+ if (_currToken != null) {
+ _lastClearedToken = _currToken;
+ _currToken = null;
+ }
+ }
+
+ @Override
+ public JsonToken getLastClearedToken() { return _lastClearedToken; }
+
+ @Override
+ public void overrideCurrentName(String name) {
+ // 14-Apr-2015, tatu: Not sure whether this can be supported, and if so,
+ // what to do with it... Delegation won't work for sure, so let's for
+ // now throw an exception
+ throw new UnsupportedOperationException("Can not currently override name during filtering read");
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, traversal
+ /**********************************************************
+ */
+
+ @Override
+ public JsonToken nextToken() throws IOException
+ {
+ // 23-May-2017, tatu: To be honest, code here is rather hairy and I don't like all
+ // conditionals; and it seems odd to return `null` but NOT considering input
+ // as closed... would love a rewrite to simplify/clear up logic here.
+
+ // Check for _allowMultipleMatches - false and at least there is one token - which is _currToken
+ // check for no buffered context _exposedContext - null
+ // If all the conditions matches then check for scalar / non-scalar property
+
+ if (!_allowMultipleMatches && (_currToken != null) && (_exposedContext == null)) {
+ // if scalar, and scalar not present in obj/array and _inclusion == ONLY_INCLUDE_ALL
+ // and INCLUDE_ALL matched once, return null
+ if (_currToken.isScalarValue() && !_headContext.isStartHandled()
+ && _inclusion == Inclusion.ONLY_INCLUDE_ALL
+ && (_itemFilter == TokenFilter.INCLUDE_ALL)) {
+ return (_currToken = null);
+ }
+ }
+ // Anything buffered?
+ TokenFilterContext ctxt = _exposedContext;
+
+ if (ctxt != null) {
+ while (true) {
+ JsonToken t = ctxt.nextTokenToRead();
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ // all done with buffered stuff?
+ if (ctxt == _headContext) {
+ _exposedContext = null;
+ if (ctxt.inArray()) {
+ t = delegate.getCurrentToken();
+ _currToken = t;
+ if (_currToken == JsonToken.END_ARRAY) {
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+ }
+ return t;
+ }
+
+ // 19-Jul-2021, tatu: [core#700]: following was commented out?!
+ // Almost! Most likely still have the current token;
+ // with the sole exception of FIELD_NAME
+ t = delegate.currentToken();
+ if (t == JsonToken.END_OBJECT) {
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+ }
+ if (t != JsonToken.FIELD_NAME) {
+ _currToken = t;
+ return t;
+ }
+ break;
+ }
+ // If not, traverse down the context chain
+ ctxt = _headContext.findChildOf(ctxt);
+ _exposedContext = ctxt;
+ if (ctxt == null) { // should never occur
+ throw _constructError("Unexpected problem: chain of filtered context broken");
+ }
+ }
+ }
+
+ // If not, need to read more. If we got any:
+ JsonToken t = delegate.nextToken();
+ if (t == null) {
+ // no strict need to close, since we have no state here
+ _currToken = t;
+ return t;
+ }
+
+ // otherwise... to include or not?
+ TokenFilter f;
+
+ switch (t.id()) {
+ case ID_START_ARRAY:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ break;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ break;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartArray();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ } else if (f != null && _inclusion == Inclusion.INCLUDE_NON_NULL) {
+ // TODO don't count as match?
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ }
+ _headContext = _headContext.createChildArrayContext(f, false);
+
+ // Also: only need buffering if parent path to be included
+ if (_inclusion == Inclusion.INCLUDE_ALL_AND_PATH) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ break;
+
+ case ID_START_OBJECT:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ break;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ break;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartObject();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ } else if (f != null && _inclusion == Inclusion.INCLUDE_NON_NULL) {
+ // TODO don't count as match?
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ }
+ _headContext = _headContext.createChildObjectContext(f, false);
+ // Also: only need buffering if parent path to be included
+ if (_inclusion == Inclusion.INCLUDE_ALL_AND_PATH) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ // note: inclusion of surrounding Object handled separately via
+ // FIELD_NAME
+ break;
+
+ case ID_END_ARRAY:
+ case ID_END_OBJECT:
+ {
+ boolean returnEnd = _headContext.isStartHandled();
+ f = _headContext.getFilter();
+ if ((f != null) && (f != TokenFilter.INCLUDE_ALL)) {
+ f.filterFinishArray();
+ }
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+ if (returnEnd) {
+ return (_currToken = t);
+ }
+ }
+ break;
+
+ case ID_FIELD_NAME:
+ {
+ final String name = delegate.getCurrentName();
+ // note: this will also set 'needToHandleName'
+ f = _headContext.setFieldName(name);
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _itemFilter = f;
+ return (_currToken = t);
+ }
+ if (f == null) {
+ delegate.nextToken();
+ delegate.skipChildren();
+ break;
+ }
+ f = f.includeProperty(name);
+ if (f == null) {
+ delegate.nextToken();
+ delegate.skipChildren();
+ break;
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ if (_verifyAllowedMatches()) {
+ if (_inclusion == Inclusion.INCLUDE_ALL_AND_PATH) {
+ return (_currToken = t);
+ }
+ } else {
+ delegate.nextToken();
+ delegate.skipChildren();
+ }
+ }
+ if (_inclusion != Inclusion.ONLY_INCLUDE_ALL) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ break;
+ }
+
+ default: // scalar value
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ return (_currToken = t);
+ }
+ if (f != null) {
+ f = _headContext.checkValue(f);
+ if ((f == TokenFilter.INCLUDE_ALL)
+ || ((f != null) && f.includeValue(delegate))) {
+ if (_verifyAllowedMatches()) {
+ return (_currToken = t);
+ }
+ }
+ }
+ // Otherwise not included (leaves must be explicitly included)
+ break;
+ }
+
+ // We get here if token was not yet found; offlined handling
+ return _nextToken2();
+ }
+
+ // Offlined handling for cases where there was no buffered token to
+ // return, and the token read next could not be returned as-is,
+ // at least not yet, but where we have not yet established that
+ // buffering is needed.
+ protected final JsonToken _nextToken2() throws IOException
+ {
+ main_loop:
+ while (true) {
+ JsonToken t = delegate.nextToken();
+ if (t == null) { // is this even legal?
+ _currToken = t;
+ return t;
+ }
+ TokenFilter f;
+
+ switch (t.id()) {
+ case ID_START_ARRAY:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartArray();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ } else if (f != null && _inclusion == Inclusion.INCLUDE_NON_NULL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return (_currToken = t);
+ }
+ _headContext = _headContext.createChildArrayContext(f, false);
+ // but if we didn't figure it out yet, need to buffer possible events
+ if (_inclusion == Inclusion.INCLUDE_ALL_AND_PATH) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ continue main_loop;
+
+ case ID_START_OBJECT:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartObject();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ } else if (f != null && _inclusion == Inclusion.INCLUDE_NON_NULL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return (_currToken = t);
+ }
+ _headContext = _headContext.createChildObjectContext(f, false);
+ if (_inclusion == Inclusion.INCLUDE_ALL_AND_PATH) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ continue main_loop;
+
+ case ID_END_ARRAY:
+ {
+ boolean returnEnd = _headContext.isStartHandled();
+ f = _headContext.getFilter();
+ if ((f != null) && (f != TokenFilter.INCLUDE_ALL)) {
+ boolean includeEmpty = f.includeEmptyArray(_headContext.hasCurrentIndex());
+ f.filterFinishArray();
+ if (includeEmpty) {
+ return _nextBuffered(_headContext);
+ }
+ }
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+ if (returnEnd) {
+ return (_currToken = t);
+ }
+ }
+ continue main_loop;
+ case ID_END_OBJECT:
+ {
+ boolean returnEnd = _headContext.isStartHandled();
+ f = _headContext.getFilter();
+ if ((f != null) && (f != TokenFilter.INCLUDE_ALL)) {
+ boolean includeEmpty = f.includeEmptyArray(_headContext.hasCurrentName());
+ f.filterFinishObject();
+ if (includeEmpty) {
+ return _nextBuffered(_headContext);
+ } }
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+ if (returnEnd) {
+ return (_currToken = t);
+ }
+ }
+ continue main_loop;
+
+ case ID_FIELD_NAME:
+ {
+ final String name = delegate.getCurrentName();
+ f = _headContext.setFieldName(name);
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _itemFilter = f;
+ return (_currToken = t);
+ }
+ if (f == null) { // filter out the value
+ delegate.nextToken();
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ f = f.includeProperty(name);
+ if (f == null) { // filter out the value
+ delegate.nextToken();
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ if (_verifyAllowedMatches()) {
+ if (_inclusion == Inclusion.INCLUDE_ALL_AND_PATH) {
+ return (_currToken = t);
+ }
+ } else {
+ delegate.nextToken();
+ delegate.skipChildren();
+ }
+ continue main_loop;
+ }
+ if (_inclusion != Inclusion.ONLY_INCLUDE_ALL) {
+ t = _nextTokenWithBuffering(_headContext);
+ if (t != null) {
+ _currToken = t;
+ return t;
+ }
+ }
+ }
+ continue main_loop;
+
+ default: // scalar value
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ return (_currToken = t);
+ }
+ if (f != null) {
+ f = _headContext.checkValue(f);
+ if ((f == TokenFilter.INCLUDE_ALL)
+ || ((f != null) && f.includeValue(delegate))) {
+ if (_verifyAllowedMatches()) {
+ return (_currToken = t);
+ }
+ }
+ }
+ // Otherwise not included (leaves must be explicitly included)
+ break;
+ }
+ }
+ }
+
+ // Method called when a new potentially included context is found.
+ protected final JsonToken _nextTokenWithBuffering(final TokenFilterContext buffRoot)
+ throws IOException
+ {
+ main_loop:
+ while (true) {
+ JsonToken t = delegate.nextToken();
+ if (t == null) { // is this even legal?
+ return t;
+ }
+ TokenFilter f;
+
+ // One simplification here: we know for a fact that the item filter is
+ // neither null nor 'include all', for most cases; the only exception
+ // being FIELD_NAME handling
+
+ switch (t.id()) {
+ case ID_START_ARRAY:
+ f = _headContext.checkValue(_itemFilter);
+ if (f == null) {
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartArray();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return _nextBuffered(buffRoot);
+ } else if (f != null && _inclusion == Inclusion.INCLUDE_NON_NULL) {
+ // TODO don't count as match?
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return _nextBuffered(buffRoot);
+ }
+ _headContext = _headContext.createChildArrayContext(f, false);
+ continue main_loop;
+
+ case ID_START_OBJECT:
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return t;
+ }
+ if (f == null) { // does this occur?
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ // Otherwise still iffy, need to check
+ f = _headContext.checkValue(f);
+ if (f == null) {
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ if (f != TokenFilter.INCLUDE_ALL) {
+ f = f.filterStartObject();
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _headContext = _headContext.createChildObjectContext(f, true);
+ return _nextBuffered(buffRoot);
+ } else if (f != null && _inclusion == Inclusion.INCLUDE_NON_NULL) {
+ // TODO don't count as match?
+ _headContext = _headContext.createChildArrayContext(f, true);
+ return _nextBuffered(buffRoot);
+ }
+ _headContext = _headContext.createChildObjectContext(f, false);
+ continue main_loop;
+
+ case ID_END_ARRAY:
+ {
+ // Unlike with other loops, here we know that content was NOT
+ // included (won't get this far otherwise)
+ f = _headContext.getFilter();
+ if ((f != null) && (f != TokenFilter.INCLUDE_ALL)) {
+ boolean includeEmpty = f.includeEmptyArray(_headContext.hasCurrentIndex());
+ f.filterFinishArray();
+ if (includeEmpty) {
+ return _nextBuffered(buffRoot);
+ }
+ }
+ boolean gotEnd = (_headContext == buffRoot);
+ boolean returnEnd = gotEnd && _headContext.isStartHandled();
+
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+
+ if (returnEnd) {
+ return t;
+ }
+ if (gotEnd) {
+ return null;
+ }
+ }
+ continue main_loop;
+ case ID_END_OBJECT:
+ {
+ // Unlike with other loops, here we know that content was NOT
+ // included (won't get this far otherwise)
+ f = _headContext.getFilter();
+ if ((f != null) && (f != TokenFilter.INCLUDE_ALL)) {
+ boolean includeEmpty = f.includeEmptyObject(_headContext.hasCurrentName());
+ f.filterFinishObject();
+ if (includeEmpty) {
+ _headContext._currentName = _headContext._parent == null
+ ? null
+ : _headContext._parent._currentName;
+ _headContext._needToHandleName = false;
+ return _nextBuffered(buffRoot);
+ }
+ }
+ boolean gotEnd = (_headContext == buffRoot);
+ boolean returnEnd = gotEnd && _headContext.isStartHandled();
+
+ _headContext = _headContext.getParent();
+ _itemFilter = _headContext.getFilter();
+
+ if (returnEnd) {
+ return t;
+ }
+ if (gotEnd) {
+ return null;
+ }
+ }
+ continue main_loop;
+
+ case ID_FIELD_NAME:
+ {
+ final String name = delegate.getCurrentName();
+ f = _headContext.setFieldName(name);
+ if (f == TokenFilter.INCLUDE_ALL) {
+ _itemFilter = f;
+ return _nextBuffered(buffRoot);
+ }
+ if (f == null) { // filter out the value
+ delegate.nextToken();
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ f = f.includeProperty(name);
+ if (f == null) { // filter out the value
+ delegate.nextToken();
+ delegate.skipChildren();
+ continue main_loop;
+ }
+ _itemFilter = f;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ if (_verifyAllowedMatches()) {
+ return _nextBuffered(buffRoot);
+ } else {
+ // edge case: if no more matches allowed, reset filter
+ // to initial state to prevent missing a token in next iteration
+ _itemFilter = _headContext.setFieldName(name);
+ }
+ }
+ }
+ continue main_loop;
+
+ default: // scalar value
+ f = _itemFilter;
+ if (f == TokenFilter.INCLUDE_ALL) {
+ return _nextBuffered(buffRoot);
+ }
+ if (f != null) {
+ f = _headContext.checkValue(f);
+ if ((f == TokenFilter.INCLUDE_ALL)
+ || ((f != null) && f.includeValue(delegate))) {
+ if (_verifyAllowedMatches()) {
+ return _nextBuffered(buffRoot);
+ }
+ }
+ }
+ // Otherwise not included (leaves must be explicitly included)
+ continue main_loop;
+ }
+ }
+ }
+
+ private JsonToken _nextBuffered(TokenFilterContext buffRoot) throws IOException
+ {
+ _exposedContext = buffRoot;
+ TokenFilterContext ctxt = buffRoot;
+ JsonToken t = ctxt.nextTokenToRead();
+ if (t != null) {
+ return t;
+ }
+ while (true) {
+ // all done with buffered stuff?
+ if (ctxt == _headContext) {
+ throw _constructError("Internal error: failed to locate expected buffered tokens");
+ /*
+ _exposedContext = null;
+ break;
+ */
+ }
+ // If not, traverse down the context chain
+ ctxt = _exposedContext.findChildOf(ctxt);
+ _exposedContext = ctxt;
+ if (ctxt == null) { // should never occur
+ throw _constructError("Unexpected problem: chain of filtered context broken");
+ }
+ t = _exposedContext.nextTokenToRead();
+ if (t != null) {
+ return t;
+ }
+ }
+ }
+
+ private final boolean _verifyAllowedMatches() throws IOException {
+ if (_matchCount == 0 || _allowMultipleMatches) {
+ ++_matchCount;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public JsonToken nextValue() throws IOException {
+ // Re-implemented same as ParserMinimalBase:
+ JsonToken t = nextToken();
+ if (t == JsonToken.FIELD_NAME) {
+ t = nextToken();
+ }
+ return t;
+ }
+
+ /**
+ * Need to override, re-implement similar to how method defined in
+ * {@link com.fasterxml.jackson.core.base.ParserMinimalBase}, to keep
+ * state correct here.
+ */
+ @Override
+ public JsonParser skipChildren() throws IOException
+ {
+ if ((_currToken != JsonToken.START_OBJECT)
+ && (_currToken != JsonToken.START_ARRAY)) {
+ return this;
+ }
+ int open = 1;
+
+ // Since proper matching of start/end markers is handled
+ // by nextToken(), we'll just count nesting levels here
+ while (true) {
+ JsonToken t = nextToken();
+ if (t == null) { // not ideal but for now, just return
+ return this;
+ }
+ if (t.isStructStart()) {
+ ++open;
+ } else if (t.isStructEnd()) {
+ if (--open == 0) {
+ return this;
+ }
+ }
+ }
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, access to token information, text
+ /**********************************************************
+ */
+
+ // 19-Jul-2021, tatu: Cannot quite just delegate these methods due to oddity
+ // of property name token, which may be buffered.
+
+ @Override public String getText() throws IOException {
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return currentName();
+ }
+ return delegate.getText();
+ }
+
+ @Override public boolean hasTextCharacters() {
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return false;
+ }
+ return delegate.hasTextCharacters();
+ }
+
+ @Override public char[] getTextCharacters() throws IOException {
+ // Not optimal but is correct, unlike delegating (as underlying stream
+ // may point to something else due to buffering)
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return currentName().toCharArray();
+ }
+ return delegate.getTextCharacters();
+ }
+
+ @Override public int getTextLength() throws IOException {
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return currentName().length();
+ }
+ return delegate.getTextLength();
+ }
+ @Override public int getTextOffset() throws IOException {
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return 0;
+ }
+ return delegate.getTextOffset();
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, access to token information, numeric
+ /**********************************************************
+ */
+
+ @Override
+ public BigInteger getBigIntegerValue() throws IOException { return delegate.getBigIntegerValue(); }
+
+ @Override
+ public boolean getBooleanValue() throws IOException { return delegate.getBooleanValue(); }
+
+ @Override
+ public byte getByteValue() throws IOException { return delegate.getByteValue(); }
+
+ @Override
+ public short getShortValue() throws IOException { return delegate.getShortValue(); }
+
+ @Override
+ public BigDecimal getDecimalValue() throws IOException { return delegate.getDecimalValue(); }
+
+ @Override
+ public double getDoubleValue() throws IOException { return delegate.getDoubleValue(); }
+
+ @Override
+ public float getFloatValue() throws IOException { return delegate.getFloatValue(); }
+
+ @Override
+ public int getIntValue() throws IOException { return delegate.getIntValue(); }
+
+ @Override
+ public long getLongValue() throws IOException { return delegate.getLongValue(); }
+
+ @Override
+ public NumberType getNumberType() throws IOException { return delegate.getNumberType(); }
+
+ @Override
+ public Number getNumberValue() throws IOException { return delegate.getNumberValue(); }
+
+ /*
+ /**********************************************************
+ /* Public API, access to token information, coercion/conversion
+ /**********************************************************
+ */
+
+ @Override public int getValueAsInt() throws IOException { return delegate.getValueAsInt(); }
+ @Override public int getValueAsInt(int defaultValue) throws IOException { return delegate.getValueAsInt(defaultValue); }
+ @Override public long getValueAsLong() throws IOException { return delegate.getValueAsLong(); }
+ @Override public long getValueAsLong(long defaultValue) throws IOException { return delegate.getValueAsLong(defaultValue); }
+ @Override public double getValueAsDouble() throws IOException { return delegate.getValueAsDouble(); }
+ @Override public double getValueAsDouble(double defaultValue) throws IOException { return delegate.getValueAsDouble(defaultValue); }
+ @Override public boolean getValueAsBoolean() throws IOException { return delegate.getValueAsBoolean(); }
+ @Override public boolean getValueAsBoolean(boolean defaultValue) throws IOException { return delegate.getValueAsBoolean(defaultValue); }
+
+ @Override public String getValueAsString() throws IOException {
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return currentName();
+ }
+ return delegate.getValueAsString();
+ }
+ @Override public String getValueAsString(String defaultValue) throws IOException {
+ if (_currToken == JsonToken.FIELD_NAME) {
+ return currentName();
+ }
+ return delegate.getValueAsString(defaultValue);
+ }
+
+ /*
+ /**********************************************************
+ /* Public API, access to token values, other
+ /**********************************************************
+ */
+
+ @Override public Object getEmbeddedObject() throws IOException { return delegate.getEmbeddedObject(); }
+ @Override public byte[] getBinaryValue(Base64Variant b64variant) throws IOException { return delegate.getBinaryValue(b64variant); }
+ @Override public int readBinaryValue(Base64Variant b64variant, OutputStream out) throws IOException { return delegate.readBinaryValue(b64variant, out); }
+ @Override public JsonLocation getTokenLocation() { return delegate.getTokenLocation(); }
+
+ /*
+ /**********************************************************
+ /* Internal helper methods
+ /**********************************************************
+ */
+
+ protected JsonStreamContext _filterContext() {
+ if (_exposedContext != null) {
+ return _exposedContext;
+ }
+ return _headContext;
+ }
+}
diff --git a/libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java b/libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java
index 9dd5975cc1659..90555c6fed455 100644
--- a/libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java
+++ b/libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java
@@ -334,6 +334,61 @@ private void testFilter(Builder expected, Builder sample, Set includes,
assertFilterResult(expected.apply(createBuilder()), filter(sample, includes, excludes, matchFieldNamesWithDots));
}
+ public void testArrayWithEmptyObjectInInclude() throws IOException {
+ testFilter(
+ builder -> builder.startObject().startArray("foo").startObject().field("bar", "baz").endObject().endArray().endObject(),
+ builder -> builder.startObject()
+ .startArray("foo")
+ .startObject()
+ .field("bar", "baz")
+ .endObject()
+ .startObject()
+ .endObject()
+ .endArray()
+ .endObject(),
+ singleton("foo.bar"),
+ emptySet(),
+ true
+ );
+ }
+
+ public void testArrayWithEmptyArrayInInclude() throws IOException {
+ testFilter(
+ builder -> builder.startObject().startArray("foo").startObject().field("bar", "baz").endObject().endArray().endObject(),
+ builder -> builder.startObject()
+ .startArray("foo")
+ .startObject()
+ .field("bar", "baz")
+ .endObject()
+ .startArray()
+ .endArray()
+ .endArray()
+ .endObject(),
+ singleton("foo.bar"),
+ emptySet(),
+ true
+ );
+ }
+
+ public void testArrayWithLastObjectSkipped() throws IOException {
+ testFilter(
+ builder -> builder.startObject().startArray("foo").startObject().field("bar", "baz").endObject().endArray().endObject(),
+ builder -> builder.startObject()
+ .startArray("foo")
+ .startObject()
+ .field("bar", "baz")
+ .endObject()
+ .startObject()
+ .field("skipped", "value")
+ .endObject()
+ .endArray()
+ .endObject(),
+ singleton("foo.bar"),
+ emptySet(),
+ true
+ );
+ }
+
protected abstract void assertFilterResult(XContentBuilder expected, XContentBuilder actual);
protected abstract XContentType getXContentType();