Skip to content

Commit 224640a

Browse files
authored
"did you mean" for ObjectParser with top named (#51018)
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
1 parent 722fdd5 commit 224640a

File tree

11 files changed

+68
-27
lines changed

11 files changed

+68
-27
lines changed

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/NamedObjectNotFoundException.java

+9-4
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,17 @@
2424
* parse for a particular name
2525
*/
2626
public class NamedObjectNotFoundException extends XContentParseException {
27+
private final Iterable<String> candidates;
2728

28-
public NamedObjectNotFoundException(String message) {
29-
this(null, message);
29+
public NamedObjectNotFoundException(XContentLocation location, String message, Iterable<String> candidates) {
30+
super(location, message);
31+
this.candidates = candidates;
3032
}
3133

32-
public NamedObjectNotFoundException(XContentLocation location, String message) {
33-
super(location, message);
34+
/**
35+
* The possible matches.
36+
*/
37+
public Iterable<String> getCandidates() {
38+
return candidates;
3439
}
3540
}

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/NamedXContentRegistry.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,18 @@ public <T, C> T parseNamedObject(Class<T> categoryClass, String name, XContentPa
123123
if (parsers == null) {
124124
if (registry.isEmpty()) {
125125
// The "empty" registry will never work so we throw a better exception as a hint.
126-
throw new NamedObjectNotFoundException("named objects are not supported for this parser");
126+
throw new XContentParseException("named objects are not supported for this parser");
127127
}
128-
throw new NamedObjectNotFoundException("unknown named object category [" + categoryClass.getName() + "]");
128+
throw new XContentParseException("unknown named object category [" + categoryClass.getName() + "]");
129129
}
130130
Entry entry = parsers.get(name);
131131
if (entry == null) {
132-
throw new NamedObjectNotFoundException(parser.getTokenLocation(), "unable to parse " + categoryClass.getSimpleName() +
133-
" with name [" + name + "]: parser not found");
132+
throw new NamedObjectNotFoundException(parser.getTokenLocation(), "unknown field [" + name + "]", parsers.keySet());
134133
}
135134
if (false == entry.name.match(name, parser.getDeprecationHandler())) {
136135
/* Note that this shouldn't happen because we already looked up the entry using the names but we need to call `match` anyway
137136
* because it is responsible for logging deprecation warnings. */
138-
throw new NamedObjectNotFoundException(parser.getTokenLocation(),
137+
throw new XContentParseException(parser.getTokenLocation(),
139138
"unable to parse " + categoryClass.getSimpleName() + " with name [" + name + "]: parser didn't match");
140139
}
141140
return categoryClass.cast(entry.parser.parse(parser, context));

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
import java.util.Arrays;
2828
import java.util.EnumSet;
2929
import java.util.HashMap;
30+
import java.util.HashSet;
3031
import java.util.List;
3132
import java.util.Map;
33+
import java.util.Set;
3234
import java.util.function.BiConsumer;
3335
import java.util.function.BiFunction;
3436
import java.util.function.Consumer;
@@ -140,8 +142,10 @@ private static <Value, Category, Context> UnknownFieldParser<Value, Context> unk
140142
try {
141143
o = parser.namedObject(categoryClass, field, context);
142144
} catch (NamedObjectNotFoundException e) {
143-
// 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!
144-
throw new XContentParseException(location, "[" + objectParser.name + "] " + e.getBareMessage(), e);
145+
Set<String> candidates = new HashSet<>(objectParser.fieldParserMap.keySet());
146+
e.getCandidates().forEach(candidates::add);
147+
String message = ErrorOnUnknown.IMPLEMENTATION.errorMessage(objectParser.name, field, candidates);
148+
throw new XContentParseException(location, message, e);
145149
}
146150
consumer.accept(value, o);
147151
};

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParseException.java

+1-8
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,6 @@ public XContentLocation getLocation() {
5959

6060
@Override
6161
public String getMessage() {
62-
return location.map(l -> "[" + l.toString() + "] ").orElse("") + getBareMessage();
63-
}
64-
65-
/**
66-
* Get the exception message without location information.
67-
*/
68-
public String getBareMessage() {
69-
return super.getMessage();
62+
return location.map(l -> "[" + l.toString() + "] ").orElse("") + super.getMessage();
7063
}
7164
}

libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.util.Map;
3939
import java.util.concurrent.atomic.AtomicReference;
4040

41+
import static org.hamcrest.Matchers.containsInAnyOrder;
4142
import static org.hamcrest.Matchers.containsString;
4243
import static org.hamcrest.Matchers.equalTo;
4344
import static org.hamcrest.Matchers.hasSize;
@@ -803,7 +804,9 @@ public void testTopLevelNamedXContent() throws IOException {
803804
{
804805
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}");
805806
XContentParseException ex = expectThrows(XContentParseException.class, () -> TopLevelNamedXConent.PARSER.parse(parser, null));
806-
assertEquals("[1:2] [test] unable to parse Object with name [not_supported_field]: parser not found", ex.getMessage());
807+
assertEquals("[1:2] [test] unknown field [not_supported_field]", ex.getMessage());
808+
NamedObjectNotFoundException cause = (NamedObjectNotFoundException) ex.getCause();
809+
assertThat(cause.getCandidates(), containsInAnyOrder("str", "int", "float", "bool"));
807810
}
808811
}
809812

rest-api-spec/src/main/resources/rest-api-spec/test/search.aggregation/30_sig_terms.yml

+15
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,18 @@
137137
search:
138138
rest_total_hits_as_int: true
139139
body: { "size" : 0, "aggs" : { "ip_terms" : { "significant_terms" : { "field" : "ip", "exclude" : "127.*" } } } }
140+
141+
---
142+
'Misspelled fields get "did you mean"':
143+
- skip:
144+
version: " - 7.99.99"
145+
reason: Implemented in 8.0 (to be backported to 7.7)
146+
- do:
147+
catch: /\[significant_terms\] unknown field \[jlp\] did you mean \[jlh\]\?/
148+
search:
149+
body:
150+
aggs:
151+
foo:
152+
significant_terms:
153+
field: foo
154+
jlp: {}

server/src/test/java/org/elasticsearch/common/xcontent/BaseXContentTestCase.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.fasterxml.jackson.core.JsonGenerationException;
2323
import com.fasterxml.jackson.core.JsonGenerator;
2424
import com.fasterxml.jackson.core.JsonParseException;
25+
2526
import org.apache.lucene.util.BytesRef;
2627
import org.apache.lucene.util.Constants;
2728
import org.elasticsearch.cluster.metadata.IndexMetaData;
@@ -78,6 +79,7 @@
7879
import static java.util.Collections.emptyMap;
7980
import static java.util.Collections.singletonMap;
8081
import static org.hamcrest.Matchers.allOf;
82+
import static org.hamcrest.Matchers.containsInAnyOrder;
8183
import static org.hamcrest.Matchers.containsString;
8284
import static org.hamcrest.Matchers.endsWith;
8385
import static org.hamcrest.Matchers.equalTo;
@@ -1169,16 +1171,17 @@ public void testNamedObject() throws IOException {
11691171
{
11701172
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class,
11711173
() -> p.namedObject(Object.class, "unknown", null));
1172-
assertThat(e.getMessage(), endsWith("unable to parse Object with name [unknown]: parser not found"));
1174+
assertThat(e.getMessage(), endsWith("unknown field [unknown]"));
1175+
assertThat(e.getCandidates(), containsInAnyOrder("test1", "test2", "deprecated", "str"));
11731176
}
11741177
{
1175-
Exception e = expectThrows(NamedObjectNotFoundException.class, () -> p.namedObject(String.class, "doesn't matter", null));
1178+
Exception e = expectThrows(XContentParseException.class, () -> p.namedObject(String.class, "doesn't matter", null));
11761179
assertEquals("unknown named object category [java.lang.String]", e.getMessage());
11771180
}
11781181
}
11791182
try (XContentParser emptyRegistryParser = xcontentType().xContent()
11801183
.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, new byte[] {})) {
1181-
Exception e = expectThrows(NamedObjectNotFoundException.class,
1184+
Exception e = expectThrows(XContentParseException.class,
11821185
() -> emptyRegistryParser.namedObject(String.class, "doesn't matter", null));
11831186
assertEquals("named objects are not supported for this parser", e.getMessage());
11841187
}

server/src/test/java/org/elasticsearch/common/xcontent/XContentParserUtilsTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public void testParseTypedKeysObject() throws IOException {
190190
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
191191
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class,
192192
() -> parseTypedKeysObject(parser, delimiter, Boolean.class, a -> {}));
193-
assertThat(e.getMessage(), endsWith("unable to parse Boolean with name [type]: parser not found"));
193+
assertThat(e.getMessage(), endsWith("unknown field [type]"));
194194
}
195195

196196
final long longValue = randomLong();

server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java

+20-1
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
import org.elasticsearch.ElasticsearchException;
3131
import org.elasticsearch.ExceptionsHelper;
3232
import org.elasticsearch.Version;
33+
import org.elasticsearch.cluster.ClusterModule;
3334
import org.elasticsearch.cluster.metadata.IndexGraveyard;
3435
import org.elasticsearch.cluster.metadata.IndexMetaData;
3536
import org.elasticsearch.cluster.metadata.MetaData;
37+
import org.elasticsearch.cluster.metadata.RepositoriesMetaData;
38+
import org.elasticsearch.common.ParseField;
3639
import org.elasticsearch.common.UUIDs;
3740
import org.elasticsearch.common.collect.ImmutableOpenMap;
3841
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
@@ -277,7 +280,7 @@ private void runLoadStateTest(boolean hasMissingCustoms, boolean preserveUnknown
277280
List<Path> dirList = Arrays.asList(dirs);
278281
Collections.shuffle(dirList, random());
279282
MetaData loadedMetaData = format.loadLatestState(logger, hasMissingCustoms ?
280-
NamedXContentRegistry.EMPTY : xContentRegistry(), dirList.toArray(new Path[0]));
283+
xContentRegistryWithMissingCustoms() : xContentRegistry(), dirList.toArray(new Path[0]));
281284
MetaData latestMetaData = meta.get(numStates-1);
282285
assertThat(loadedMetaData.clusterUUID(), not(equalTo("_na_")));
283286
assertThat(loadedMetaData.clusterUUID(), equalTo(latestMetaData.clusterUUID()));
@@ -706,4 +709,20 @@ public long addDummyFiles(String prefix, Path... paths) throws IOException {
706709
}
707710
return realId + 1;
708711
}
712+
713+
/**
714+
* The {@link NamedXContentRegistry} to use for most {@link XContentParser} in this test.
715+
*/
716+
protected final NamedXContentRegistry xContentRegistry() {
717+
return new NamedXContentRegistry(ClusterModule.getNamedXWriteables());
718+
}
719+
720+
/**
721+
* The {@link NamedXContentRegistry} to use for {@link XContentParser}s that should be
722+
* missing all of the normal cluster state parsers.
723+
*/
724+
protected NamedXContentRegistry xContentRegistryWithMissingCustoms() {
725+
return new NamedXContentRegistry(Arrays.asList(
726+
new NamedXContentRegistry.Entry(MetaData.Custom.class, new ParseField("garbage"), RepositoriesMetaData::fromXContent)));
727+
}
709728
}

server/src/test/java/org/elasticsearch/search/rescore/QueryRescorerBuilderTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ public void testUnknownFieldsExpection() throws IOException {
223223
"}\n";
224224
try (XContentParser parser = createParser(rescoreElement)) {
225225
Exception e = expectThrows(NamedObjectNotFoundException.class, () -> RescorerBuilder.parseFromXContent(parser));
226-
assertEquals("[3:27] unable to parse RescorerBuilder with name [bad_rescorer_name]: parser not found", e.getMessage());
226+
assertEquals("[3:27] unknown field [bad_rescorer_name]", e.getMessage());
227227
}
228228
rescoreElement = "{\n" +
229229
" \"bad_fieldName\" : 20\n" +

server/src/test/java/org/elasticsearch/search/suggest/SuggestionTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public void testUnknownSuggestionTypeThrows() throws IOException {
187187
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation);
188188
ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser::getTokenLocation);
189189
NamedObjectNotFoundException e = expectThrows(NamedObjectNotFoundException.class, () -> Suggestion.fromXContent(parser));
190-
assertEquals("[1:31] unable to parse Suggestion with name [unknownType]: parser not found", e.getMessage());
190+
assertEquals("[1:31] unknown field [unknownType]", e.getMessage());
191191
}
192192
}
193193

0 commit comments

Comments
 (0)