org.elasticsearch.index.mapper.DotExpandingXContentParser Maven / Gradle / Ivy
/*
* 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.BooleanSupplier;
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 BooleanSupplier isWithinLeafObject;
final Deque parsers = new ArrayDeque<>();
WrappingParser(XContentParser in, BooleanSupplier isWithinLeafObject) throws IOException {
this.isWithinLeafObject = isWithinLeafObject;
parsers.push(in);
if (in.currentToken() == Token.FIELD_NAME) {
expandDots();
}
}
@Override
public Token nextToken() throws IOException {
Token token;
while ((token = delegate().nextToken()) == null) {
parsers.pop();
if (parsers.isEmpty()) {
return null;
}
}
if (token != Token.FIELD_NAME) {
return token;
}
expandDots();
return Token.FIELD_NAME;
}
private void expandDots() 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 (isWithinLeafObject.getAsBoolean()) {
return;
}
XContentParser delegate = delegate();
String field = delegate.currentName();
String[] subpaths = splitAndValidatePath(field);
if (subpaths.length == 0) {
throw new IllegalArgumentException("field name cannot contain only dots: [" + field + "]");
}
// 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 (subpaths.length == 1 && field.endsWith(".") == false) {
return;
}
XContentLocation location = delegate.getTokenLocation();
Token token = delegate.nextToken();
if (token == Token.END_OBJECT || token == Token.END_ARRAY) {
throw new IllegalStateException("Expecting START_OBJECT or START_ARRAY or VALUE but got [" + token + "]");
} else {
XContentParser subParser = token == Token.START_OBJECT || token == Token.START_ARRAY
? new XContentSubParser(delegate)
: new SingletonValueXContentParser(delegate);
parsers.push(new DotExpandingXContentParser(subParser, subpaths, location, isWithinLeafObject));
}
}
@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