When you declare an ObjectParser with top level named objects like we do with `significant_terms` we didn't support "did you mean". This fixes that. Relates #50938
This commit is contained in:
parent
4fc865e579
commit
5299664ae3
|
@ -24,12 +24,17 @@ package org.elasticsearch.common.xcontent;
|
||||||
* parse for a particular name
|
* parse for a particular name
|
||||||
*/
|
*/
|
||||||
public class NamedObjectNotFoundException extends XContentParseException {
|
public class NamedObjectNotFoundException extends XContentParseException {
|
||||||
|
private final Iterable<String> candidates;
|
||||||
|
|
||||||
public NamedObjectNotFoundException(String message) {
|
public NamedObjectNotFoundException(XContentLocation location, String message, Iterable<String> candidates) {
|
||||||
this(null, message);
|
super(location, message);
|
||||||
|
this.candidates = candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NamedObjectNotFoundException(XContentLocation location, String message) {
|
/**
|
||||||
super(location, message);
|
* The possible matches.
|
||||||
|
*/
|
||||||
|
public Iterable<String> getCandidates() {
|
||||||
|
return candidates;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,19 +123,18 @@ public class NamedXContentRegistry {
|
||||||
if (parsers == null) {
|
if (parsers == null) {
|
||||||
if (registry.isEmpty()) {
|
if (registry.isEmpty()) {
|
||||||
// The "empty" registry will never work so we throw a better exception as a hint.
|
// The "empty" registry will never work so we throw a better exception as a hint.
|
||||||
throw new NamedObjectNotFoundException("named objects are not supported for this parser");
|
throw new XContentParseException("named objects are not supported for this parser");
|
||||||
}
|
}
|
||||||
throw new NamedObjectNotFoundException("unknown named object category [" + categoryClass.getName() + "]");
|
throw new XContentParseException("unknown named object category [" + categoryClass.getName() + "]");
|
||||||
}
|
}
|
||||||
Entry entry = parsers.get(name);
|
Entry entry = parsers.get(name);
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
throw new NamedObjectNotFoundException(parser.getTokenLocation(), "unable to parse " + categoryClass.getSimpleName() +
|
throw new NamedObjectNotFoundException(parser.getTokenLocation(), "unknown field [" + name + "]", parsers.keySet());
|
||||||
" with name [" + name + "]: parser not found");
|
|
||||||
}
|
}
|
||||||
if (false == entry.name.match(name, parser.getDeprecationHandler())) {
|
if (false == entry.name.match(name, parser.getDeprecationHandler())) {
|
||||||
/* Note that this shouldn't happen because we already looked up the entry using the names but we need to call `match` anyway
|
/* Note that this shouldn't happen because we already looked up the entry using the names but we need to call `match` anyway
|
||||||
* because it is responsible for logging deprecation warnings. */
|
* because it is responsible for logging deprecation warnings. */
|
||||||
throw new NamedObjectNotFoundException(parser.getTokenLocation(),
|
throw new XContentParseException(parser.getTokenLocation(),
|
||||||
"unable to parse " + categoryClass.getSimpleName() + " with name [" + name + "]: parser didn't match");
|
"unable to parse " + categoryClass.getSimpleName() + " with name [" + name + "]: parser didn't match");
|
||||||
}
|
}
|
||||||
return categoryClass.cast(entry.parser.parse(parser, context));
|
return categoryClass.cast(entry.parser.parse(parser, context));
|
||||||
|
|
|
@ -27,8 +27,10 @@ import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
@ -140,8 +142,10 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
|
||||||
try {
|
try {
|
||||||
o = parser.namedObject(categoryClass, field, context);
|
o = parser.namedObject(categoryClass, field, context);
|
||||||
} catch (NamedObjectNotFoundException e) {
|
} catch (NamedObjectNotFoundException e) {
|
||||||
// TODO It'd be lovely if we could the options here but we don't have the right stuff plumbed through. We'll get to it!
|
Set<String> candidates = new HashSet<>(objectParser.fieldParserMap.keySet());
|
||||||
throw new XContentParseException(location, "[" + objectParser.name + "] " + e.getBareMessage(), e);
|
e.getCandidates().forEach(candidates::add);
|
||||||
|
String message = ErrorOnUnknown.IMPLEMENTATION.errorMessage(objectParser.name, field, candidates);
|
||||||
|
throw new XContentParseException(location, message, e);
|
||||||
}
|
}
|
||||||
consumer.accept(value, o);
|
consumer.accept(value, o);
|
||||||
};
|
};
|
||||||
|
|
|
@ -59,13 +59,6 @@ public class XContentParseException extends IllegalArgumentException {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
return location.map(l -> "[" + l.toString() + "] ").orElse("") + getBareMessage();
|
return location.map(l -> "[" + l.toString() + "] ").orElse("") + super.getMessage();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the exception message without location information.
|
|
||||||
*/
|
|
||||||
public String getBareMessage() {
|
|
||||||
return super.getMessage();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
@ -804,7 +805,9 @@ public class ObjectParserTests extends ESTestCase {
|
||||||
{
|
{
|
||||||
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}");
|
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}");
|
||||||
XContentParseException ex = expectThrows(XContentParseException.class, () -> TopLevelNamedXConent.PARSER.parse(parser, null));
|
XContentParseException ex = expectThrows(XContentParseException.class, () -> TopLevelNamedXConent.PARSER.parse(parser, null));
|
||||||
assertEquals("[1:2] [test] unable to parse Object with name [not_supported_field]: parser not found", ex.getMessage());
|
assertEquals("[1:2] [test] unknown field [not_supported_field]", ex.getMessage());
|
||||||
|
NamedObjectNotFoundException cause = (NamedObjectNotFoundException) ex.getCause();
|
||||||
|
assertThat(cause.getCandidates(), containsInAnyOrder("str", "int", "float", "bool"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,3 +137,18 @@
|
||||||
search:
|
search:
|
||||||
rest_total_hits_as_int: true
|
rest_total_hits_as_int: true
|
||||||
body: { "size" : 0, "aggs" : { "ip_terms" : { "significant_terms" : { "field" : "ip", "exclude" : "127.*" } } } }
|
body: { "size" : 0, "aggs" : { "ip_terms" : { "significant_terms" : { "field" : "ip", "exclude" : "127.*" } } } }
|
||||||
|
|
||||||
|
---
|
||||||
|
'Misspelled fields get "did you mean"':
|
||||||
|
- skip:
|
||||||
|
version: " - 7.99.99"
|
||||||
|
reason: Implemented in 8.0 (to be backported to 7.7)
|
||||||
|
- do:
|
||||||
|
catch: /\[significant_terms\] unknown field \[jlp\] did you mean \[jlh\]\?/
|
||||||
|
search:
|
||||||
|
body:
|
||||||
|
aggs:
|
||||||
|
foo:
|
||||||
|
significant_terms:
|
||||||
|
field: foo
|
||||||
|
jlp: {}
|
||||||
|
|
|
@ -22,6 +22,7 @@ package org.elasticsearch.common.xcontent;
|
||||||
import com.fasterxml.jackson.core.JsonGenerationException;
|
import com.fasterxml.jackson.core.JsonGenerationException;
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.core.JsonParseException;
|
import com.fasterxml.jackson.core.JsonParseException;
|
||||||
|
|
||||||
import org.apache.lucene.util.BytesRef;
|
import org.apache.lucene.util.BytesRef;
|
||||||
import org.apache.lucene.util.Constants;
|
import org.apache.lucene.util.Constants;
|
||||||
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
||||||
|
@ -78,6 +79,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
import static java.util.Collections.emptyMap;
|
import static java.util.Collections.emptyMap;
|
||||||
import static java.util.Collections.singletonMap;
|
import static java.util.Collections.singletonMap;
|
||||||
import static org.hamcrest.Matchers.allOf;
|
import static org.hamcrest.Matchers.allOf;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.endsWith;
|
import static org.hamcrest.Matchers.endsWith;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
@ -1169,16 +1171,17 @@ public abstract class BaseXContentTestCase extends ESTestCase {
|
||||||
{
|
{
|
||||||
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class,
|
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class,
|
||||||
() -> p.namedObject(Object.class, "unknown", null));
|
() -> p.namedObject(Object.class, "unknown", null));
|
||||||
assertThat(e.getMessage(), endsWith("unable to parse Object with name [unknown]: parser not found"));
|
assertThat(e.getMessage(), endsWith("unknown field [unknown]"));
|
||||||
|
assertThat(e.getCandidates(), containsInAnyOrder("test1", "test2", "deprecated", "str"));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
Exception e = expectThrows(NamedObjectNotFoundException.class, () -> p.namedObject(String.class, "doesn't matter", null));
|
Exception e = expectThrows(XContentParseException.class, () -> p.namedObject(String.class, "doesn't matter", null));
|
||||||
assertEquals("unknown named object category [java.lang.String]", e.getMessage());
|
assertEquals("unknown named object category [java.lang.String]", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try (XContentParser emptyRegistryParser = xcontentType().xContent()
|
try (XContentParser emptyRegistryParser = xcontentType().xContent()
|
||||||
.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, new byte[] {})) {
|
.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, new byte[] {})) {
|
||||||
Exception e = expectThrows(NamedObjectNotFoundException.class,
|
Exception e = expectThrows(XContentParseException.class,
|
||||||
() -> emptyRegistryParser.namedObject(String.class, "doesn't matter", null));
|
() -> emptyRegistryParser.namedObject(String.class, "doesn't matter", null));
|
||||||
assertEquals("named objects are not supported for this parser", e.getMessage());
|
assertEquals("named objects are not supported for this parser", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,7 +190,7 @@ public class XContentParserUtilsTests extends ESTestCase {
|
||||||
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
|
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
|
||||||
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class,
|
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class,
|
||||||
() -> parseTypedKeysObject(parser, delimiter, Boolean.class, a -> {}));
|
() -> parseTypedKeysObject(parser, delimiter, Boolean.class, a -> {}));
|
||||||
assertThat(e.getMessage(), endsWith("unable to parse Boolean with name [type]: parser not found"));
|
assertThat(e.getMessage(), endsWith("unknown field [type]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
final long longValue = randomLong();
|
final long longValue = randomLong();
|
||||||
|
|
|
@ -30,9 +30,12 @@ import org.apache.lucene.util.LuceneTestCase;
|
||||||
import org.elasticsearch.ElasticsearchException;
|
import org.elasticsearch.ElasticsearchException;
|
||||||
import org.elasticsearch.ExceptionsHelper;
|
import org.elasticsearch.ExceptionsHelper;
|
||||||
import org.elasticsearch.Version;
|
import org.elasticsearch.Version;
|
||||||
|
import org.elasticsearch.cluster.ClusterModule;
|
||||||
import org.elasticsearch.cluster.metadata.IndexGraveyard;
|
import org.elasticsearch.cluster.metadata.IndexGraveyard;
|
||||||
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
import org.elasticsearch.cluster.metadata.IndexMetaData;
|
||||||
import org.elasticsearch.cluster.metadata.MetaData;
|
import org.elasticsearch.cluster.metadata.MetaData;
|
||||||
|
import org.elasticsearch.cluster.metadata.RepositoriesMetaData;
|
||||||
|
import org.elasticsearch.common.ParseField;
|
||||||
import org.elasticsearch.common.UUIDs;
|
import org.elasticsearch.common.UUIDs;
|
||||||
import org.elasticsearch.common.collect.ImmutableOpenMap;
|
import org.elasticsearch.common.collect.ImmutableOpenMap;
|
||||||
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
||||||
|
@ -277,7 +280,7 @@ public class MetaDataStateFormatTests extends ESTestCase {
|
||||||
List<Path> dirList = Arrays.asList(dirs);
|
List<Path> dirList = Arrays.asList(dirs);
|
||||||
Collections.shuffle(dirList, random());
|
Collections.shuffle(dirList, random());
|
||||||
MetaData loadedMetaData = format.loadLatestState(logger, hasMissingCustoms ?
|
MetaData loadedMetaData = format.loadLatestState(logger, hasMissingCustoms ?
|
||||||
NamedXContentRegistry.EMPTY : xContentRegistry(), dirList.toArray(new Path[0]));
|
xContentRegistryWithMissingCustoms() : xContentRegistry(), dirList.toArray(new Path[0]));
|
||||||
MetaData latestMetaData = meta.get(numStates-1);
|
MetaData latestMetaData = meta.get(numStates-1);
|
||||||
assertThat(loadedMetaData.clusterUUID(), not(equalTo("_na_")));
|
assertThat(loadedMetaData.clusterUUID(), not(equalTo("_na_")));
|
||||||
assertThat(loadedMetaData.clusterUUID(), equalTo(latestMetaData.clusterUUID()));
|
assertThat(loadedMetaData.clusterUUID(), equalTo(latestMetaData.clusterUUID()));
|
||||||
|
@ -706,4 +709,20 @@ public class MetaDataStateFormatTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
return realId + 1;
|
return realId + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link NamedXContentRegistry} to use for most {@link XContentParser} in this test.
|
||||||
|
*/
|
||||||
|
protected final NamedXContentRegistry xContentRegistry() {
|
||||||
|
return new NamedXContentRegistry(ClusterModule.getNamedXWriteables());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link NamedXContentRegistry} to use for {@link XContentParser}s that should be
|
||||||
|
* missing all of the normal cluster state parsers.
|
||||||
|
*/
|
||||||
|
protected NamedXContentRegistry xContentRegistryWithMissingCustoms() {
|
||||||
|
return new NamedXContentRegistry(Arrays.asList(
|
||||||
|
new NamedXContentRegistry.Entry(MetaData.Custom.class, new ParseField("garbage"), RepositoriesMetaData::fromXContent)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,7 +223,7 @@ public class QueryRescorerBuilderTests extends ESTestCase {
|
||||||
"}\n";
|
"}\n";
|
||||||
try (XContentParser parser = createParser(rescoreElement)) {
|
try (XContentParser parser = createParser(rescoreElement)) {
|
||||||
Exception e = expectThrows(NamedObjectNotFoundException.class, () -> RescorerBuilder.parseFromXContent(parser));
|
Exception e = expectThrows(NamedObjectNotFoundException.class, () -> RescorerBuilder.parseFromXContent(parser));
|
||||||
assertEquals("[3:27] unable to parse RescorerBuilder with name [bad_rescorer_name]: parser not found", e.getMessage());
|
assertEquals("[3:27] unknown field [bad_rescorer_name]", e.getMessage());
|
||||||
}
|
}
|
||||||
rescoreElement = "{\n" +
|
rescoreElement = "{\n" +
|
||||||
" \"bad_fieldName\" : 20\n" +
|
" \"bad_fieldName\" : 20\n" +
|
||||||
|
|
|
@ -187,7 +187,7 @@ public class SuggestionTests extends ESTestCase {
|
||||||
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation);
|
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation);
|
||||||
ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser::getTokenLocation);
|
ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser::getTokenLocation);
|
||||||
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class, () -> Suggestion.fromXContent(parser));
|
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class, () -> Suggestion.fromXContent(parser));
|
||||||
assertEquals("[1:31] unable to parse Suggestion with name [unknownType]: parser not found", e.getMessage());
|
assertEquals("[1:31] unknown field [unknownType]", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue