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();