org.elasticsearch.index.mapper.DotExpandingXContentParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of elasticsearch Show documentation
Show all versions of elasticsearch Show documentation
Elasticsearch - Open Source, Distributed, RESTful Search Engine
/*
* 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.index.mapper;
import org.elasticsearch.core.CheckedFunction;
import org.elasticsearch.xcontent.FilterXContentParser;
import org.elasticsearch.xcontent.FilterXContentParserWrapper;
import org.elasticsearch.xcontent.XContentLocation;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentSubParser;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/**
* An XContentParser that reinterprets field names containing dots as an object structure.
*
* A field name named {@code "foo.bar.baz":...} will be parsed instead as {@code 'foo':{'bar':{'baz':...}}}.
* The token location is preserved so that error messages refer to the original content being parsed.
* This parser can output duplicate keys, but that is fine given that it's used for document parsing. The mapping
* lookups will return the same mapper/field type, and we never load incoming documents in a map where duplicate
* keys would end up overriding each other.
*/
class DotExpandingXContentParser extends FilterXContentParserWrapper {
private static final class WrappingParser extends FilterXContentParser {
private final ContentPath contentPath;
final Deque parsers = new ArrayDeque<>();
WrappingParser(XContentParser in, ContentPath contentPath) throws IOException {
this.contentPath = contentPath;
parsers.push(in);
if (in.currentToken() == Token.FIELD_NAME) {
expandDots(in);
}
}
@Override
public Token nextToken() throws IOException {
Token token;
XContentParser delegate;
while ((token = (delegate = parsers.peek()).nextToken()) == null) {
parsers.pop();
if (parsers.isEmpty()) {
return null;
}
}
if (token != Token.FIELD_NAME) {
return token;
}
expandDots(delegate);
return Token.FIELD_NAME;
}
private void expandDots(XContentParser delegate) throws IOException {
// this handles fields that belong to objects that can't hold subobjects, where the document specifies
// the object holding the flat fields
// e.g. { "metrics.service": { "time.max" : 10 } } with service having subobjects set to false
if (contentPath.isWithinLeafObject()) {
return;
}
String field = delegate.currentName();
int length = field.length();
if (length == 0) {
throw new IllegalArgumentException("field name cannot be an empty string");
}
final int dotCount = FieldTypeLookup.dotCount(field);
if (dotCount == 0) {
return;
}
doExpandDots(delegate, field, dotCount);
}
private void doExpandDots(XContentParser delegate, String field, int dotCount) throws IOException {
int next;
int offset = 0;
String[] list = new String[dotCount + 1];
int listIndex = 0;
for (int i = 0; i < dotCount; i++) {
next = field.indexOf('.', offset);
list[listIndex++] = field.substring(offset, next);
offset = next + 1;
}
// Add remaining segment
list[listIndex] = field.substring(offset);
int resultSize = list.length;
// Construct result
while (resultSize > 0 && list[resultSize - 1].isEmpty()) {
resultSize--;
}
if (resultSize == 0) {
throw new IllegalArgumentException("field name cannot contain only dots");
}
final String[] subpaths;
if (resultSize == list.length) {
for (String part : list) {
// check if the field name contains only whitespace
if (part.isBlank()) {
throwOnBlankOrEmptyPart(field, part);
}
}
subpaths = list;
} else {
// Corner case: if the input has a single trailing '.', eg 'field.', then we will get a single
// subpath due to the way String.split() works. We can only return fast here if this is not
// the case
// TODO make this case throw an error instead? https://github.com/elastic/elasticsearch/issues/28948
if (resultSize == 1 && field.endsWith(".") == false) {
return;
}
subpaths = extractAndValidateResults(field, list, resultSize);
}
pushSubParser(delegate, subpaths);
}
private void pushSubParser(XContentParser delegate, String[] subpaths) throws IOException {
XContentLocation location = delegate.getTokenLocation();
Token token = delegate.nextToken();
final XContentParser subParser;
if (token == Token.START_OBJECT || token == Token.START_ARRAY) {
subParser = new XContentSubParser(delegate);
} else {
if (token == Token.END_OBJECT || token == Token.END_ARRAY) {
throwExpectedOpen(token);
}
subParser = new SingletonValueXContentParser(delegate);
}
parsers.push(new DotExpandingXContentParser(subParser, subpaths, location, contentPath));
}
private static void throwExpectedOpen(Token token) {
throw new IllegalStateException("Expecting START_OBJECT or START_ARRAY or VALUE but got [" + token + "]");
}
private static String[] extractAndValidateResults(String field, String[] list, int resultSize) {
final String[] subpaths = new String[resultSize];
for (int i = 0; i < resultSize; i++) {
String part = list[i];
// check if the field name contains only whitespace
if (part.isBlank()) {
throwOnBlankOrEmptyPart(field, part);
}
subpaths[i] = part;
}
return subpaths;
}
private static void throwOnBlankOrEmptyPart(String field, String part) {
if (part.isEmpty()) {
throw new IllegalArgumentException("field name cannot contain only whitespace: ['" + field + "']");
}
throw new IllegalArgumentException(
"field name starting or ending with a [.] makes object resolution ambiguous: [" + field + "]"
);
}
@Override
protected XContentParser delegate() {
return parsers.peek();
}
@Override
public Map map() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public Map mapOrdered() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public Map mapStrings() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public Map map(Supplier
© 2015 - 2025 Weber Informatics LLC | Privacy Policy