Add did you mean to strict REST params
This commit adds a did you mean feature to the strict REST params error message. This works by comparing any unconsumed parameters to all of the consumer parameters, comparing the Levenstein distance between those parameters, and taking any consumed parameters that are close to an unconsumed parameter as candiates for the did you mean. * Fix pluralization in strict REST params message This commit fixes the pluralization in the strict REST parameters error message so that the word "parameter" is not unconditionally written as "parameters" even when there is only one unrecognized parameter. * Strength strict REST params did you mean test This commit adds an unconsumed parameter that is too far from every consumed parameter to have any candidate suggestions. Relates #20747
This commit is contained in:
parent
4b82703bf7
commit
d7dca15970
|
@ -19,8 +19,11 @@
|
|||
|
||||
package org.elasticsearch.rest;
|
||||
|
||||
import org.apache.lucene.search.spell.LevensteinDistance;
|
||||
import org.apache.lucene.util.CollectionUtil;
|
||||
import org.elasticsearch.client.node.NodeClient;
|
||||
import org.elasticsearch.common.ParseFieldMatcher;
|
||||
import org.elasticsearch.common.collect.Tuple;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.settings.Setting;
|
||||
import org.elasticsearch.common.settings.Setting.Property;
|
||||
|
@ -28,10 +31,13 @@ import org.elasticsearch.common.settings.Settings;
|
|||
import org.elasticsearch.plugins.ActionPlugin;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
|
@ -59,16 +65,44 @@ public abstract class BaseRestHandler extends AbstractComponent implements RestH
|
|||
final RestChannelConsumer action = prepareRequest(request, client);
|
||||
|
||||
// validate unconsumed params, but we must exclude params used to format the response
|
||||
final List<String> unconsumedParams =
|
||||
request.unconsumedParams().stream().filter(p -> !responseParams().contains(p)).collect(Collectors.toList());
|
||||
// use a sorted set so the unconsumed parameters appear in a reliable sorted order
|
||||
final SortedSet<String> unconsumedParams =
|
||||
request.unconsumedParams().stream().filter(p -> !responseParams().contains(p)).collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
// validate the non-response params
|
||||
if (!unconsumedParams.isEmpty()) {
|
||||
final String message = String.format(
|
||||
String message = String.format(
|
||||
Locale.ROOT,
|
||||
"request [%s] contains unrecognized parameters: %s",
|
||||
"request [%s] contains unrecognized parameter%s: ",
|
||||
request.path(),
|
||||
unconsumedParams.toString());
|
||||
unconsumedParams.size() > 1 ? "s" : "");
|
||||
boolean first = true;
|
||||
for (final String unconsumedParam : unconsumedParams) {
|
||||
final LevensteinDistance ld = new LevensteinDistance();
|
||||
final List<Tuple<Float, String>> scoredParams = new ArrayList<>();
|
||||
for (String consumedParam : request.consumedParams()) {
|
||||
final float distance = ld.getDistance(unconsumedParam, consumedParam);
|
||||
if (distance > 0.5f) {
|
||||
scoredParams.add(new Tuple<>(distance, consumedParam));
|
||||
}
|
||||
}
|
||||
CollectionUtil.timSort(scoredParams, (a, b) -> {
|
||||
// sort by distance in reverse order, then parameter name for equal distances
|
||||
int compare = a.v1().compareTo(b.v1());
|
||||
if (compare != 0) return -compare;
|
||||
else return a.v2().compareTo(b.v2());
|
||||
});
|
||||
if (first == false) {
|
||||
message += ", ";
|
||||
}
|
||||
message += "[" + unconsumedParam + "]";
|
||||
final List<String> keys = scoredParams.stream().map(Tuple::v2).collect(Collectors.toList());
|
||||
if (keys.isEmpty() == false) {
|
||||
message += " -> did you mean " + (keys.size() == 1 ? "[" + keys.get(0) + "]": "any of " + keys.toString()) + "?";
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.elasticsearch.common.unit.TimeValue;
|
|||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -129,6 +128,16 @@ public abstract class RestRequest implements ToXContent.Params {
|
|||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of parameters that have been consumed. This method returns a copy, callers
|
||||
* are free to modify the returned list.
|
||||
*
|
||||
* @return the list of currently consumed parameters.
|
||||
*/
|
||||
List<String> consumedParams() {
|
||||
return consumedParams.stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of parameters that have not yet been consumed. This method returns a copy,
|
||||
* callers are free to modify the returned list.
|
||||
|
|
|
@ -73,8 +73,9 @@ public class RestAnalyzeAction extends BaseRestHandler {
|
|||
analyzeRequest.text(texts);
|
||||
analyzeRequest.analyzer(request.param("analyzer"));
|
||||
analyzeRequest.field(request.param("field"));
|
||||
if (request.hasParam("tokenizer")) {
|
||||
analyzeRequest.tokenizer(request.param("tokenizer"));
|
||||
final String tokenizer = request.param("tokenizer");
|
||||
if (tokenizer != null) {
|
||||
analyzeRequest.tokenizer(tokenizer);
|
||||
}
|
||||
for (String filter : request.paramAsStringArray("filter", Strings.EMPTY_ARRAY)) {
|
||||
analyzeRequest.addTokenFilter(filter);
|
||||
|
|
|
@ -33,14 +33,34 @@ import java.util.HashMap;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.hamcrest.core.AnyOf.anyOf;
|
||||
import static org.hamcrest.core.StringContains.containsString;
|
||||
import static org.hamcrest.object.HasToString.hasToString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class BaseRestHandlerTests extends ESTestCase {
|
||||
|
||||
public void testUnconsumedParameters() throws Exception {
|
||||
public void testOneUnconsumedParameters() throws Exception {
|
||||
final AtomicBoolean executed = new AtomicBoolean();
|
||||
BaseRestHandler handler = new BaseRestHandler(Settings.EMPTY) {
|
||||
@Override
|
||||
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
request.param("consumed");
|
||||
return channel -> executed.set(true);
|
||||
}
|
||||
};
|
||||
|
||||
final HashMap<String, String> params = new HashMap<>();
|
||||
params.put("consumed", randomAsciiOfLength(8));
|
||||
params.put("unconsumed", randomAsciiOfLength(8));
|
||||
RestRequest request = new FakeRestRequest.Builder().withParams(params).build();
|
||||
RestChannel channel = new FakeRestChannel(request, randomBoolean(), 1);
|
||||
final IllegalArgumentException e =
|
||||
expectThrows(IllegalArgumentException.class, () -> handler.handleRequest(request, channel, mock(NodeClient.class)));
|
||||
assertThat(e, hasToString(containsString("request [/] contains unrecognized parameter: [unconsumed]")));
|
||||
assertFalse(executed.get());
|
||||
}
|
||||
|
||||
public void testMultipleUnconsumedParameters() throws Exception {
|
||||
final AtomicBoolean executed = new AtomicBoolean();
|
||||
BaseRestHandler handler = new BaseRestHandler(Settings.EMPTY) {
|
||||
@Override
|
||||
|
@ -56,14 +76,44 @@ public class BaseRestHandlerTests extends ESTestCase {
|
|||
params.put("unconsumed-second", randomAsciiOfLength(8));
|
||||
RestRequest request = new FakeRestRequest.Builder().withParams(params).build();
|
||||
RestChannel channel = new FakeRestChannel(request, randomBoolean(), 1);
|
||||
final IllegalArgumentException e =
|
||||
expectThrows(IllegalArgumentException.class, () -> handler.handleRequest(request, channel, mock(NodeClient.class)));
|
||||
assertThat(e, hasToString(containsString("request [/] contains unrecognized parameters: [unconsumed-first], [unconsumed-second]")));
|
||||
assertFalse(executed.get());
|
||||
}
|
||||
|
||||
public void testUnconsumedParametersDidYouMean() throws Exception {
|
||||
final AtomicBoolean executed = new AtomicBoolean();
|
||||
BaseRestHandler handler = new BaseRestHandler(Settings.EMPTY) {
|
||||
@Override
|
||||
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
|
||||
request.param("consumed");
|
||||
request.param("field");
|
||||
request.param("tokenizer");
|
||||
request.param("very_close_to_parameter_1");
|
||||
request.param("very_close_to_parameter_2");
|
||||
return channel -> executed.set(true);
|
||||
}
|
||||
};
|
||||
|
||||
final HashMap<String, String> params = new HashMap<>();
|
||||
params.put("consumed", randomAsciiOfLength(8));
|
||||
params.put("flied", randomAsciiOfLength(8));
|
||||
params.put("tokenzier", randomAsciiOfLength(8));
|
||||
params.put("very_close_to_parametre", randomAsciiOfLength(8));
|
||||
params.put("very_far_from_every_consumed_parameter", randomAsciiOfLength(8));
|
||||
RestRequest request = new FakeRestRequest.Builder().withParams(params).build();
|
||||
RestChannel channel = new FakeRestChannel(request, randomBoolean(), 1);
|
||||
final IllegalArgumentException e =
|
||||
expectThrows(IllegalArgumentException.class, () -> handler.handleRequest(request, channel, mock(NodeClient.class)));
|
||||
assertThat(
|
||||
e,
|
||||
// we can not rely on ordering of the unconsumed parameters here
|
||||
anyOf(
|
||||
hasToString(containsString("request [/] contains unrecognized parameters: [unconsumed-first, unconsumed-second]")),
|
||||
hasToString(containsString("request [/] contains unrecognized parameters: [unconsumed-second, unconsumed-first]"))));
|
||||
hasToString(containsString(
|
||||
"request [/] contains unrecognized parameters: " +
|
||||
"[flied] -> did you mean [field]?, " +
|
||||
"[tokenzier] -> did you mean [tokenizer]?, " +
|
||||
"[very_close_to_parametre] -> did you mean any of [very_close_to_parameter_1, very_close_to_parameter_2]?, " +
|
||||
"[very_far_from_every_consumed_parameter]")));
|
||||
assertFalse(executed.get());
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue