Refactor PathTrie and RestController to use a single trie for all methods (#25459)
* Refactor PathTrie and RestController to use a single trie for all methods This changes `PathTrie` and `RestController` to use a single `PathTrie` for all endpoints, it also allows retrieving the endpoints' supported HTTP methods more easily. This is a spin-off and prerequisite of #24437 * Use EnumSet instead of multiple if conditions * Make MethodHandlers package-private and final * Remove duplicate registerHandler method * Remove public modifier
This commit is contained in:
parent
6e5cc424a8
commit
30b5ca7ab7
|
@ -19,14 +19,49 @@
|
||||||
|
|
||||||
package org.elasticsearch.common.path;
|
package org.elasticsearch.common.path;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static java.util.Collections.emptyMap;
|
import static java.util.Collections.emptyMap;
|
||||||
import static java.util.Collections.unmodifiableMap;
|
import static java.util.Collections.unmodifiableMap;
|
||||||
|
|
||||||
public class PathTrie<T> {
|
public class PathTrie<T> {
|
||||||
|
|
||||||
|
enum TrieMatchingMode {
|
||||||
|
/*
|
||||||
|
* Retrieve only explicitly mapped nodes, no wildcards are
|
||||||
|
* matched.
|
||||||
|
*/
|
||||||
|
EXPLICIT_NODES_ONLY,
|
||||||
|
/*
|
||||||
|
* Retrieve only explicitly mapped nodes, with wildcards
|
||||||
|
* allowed as root nodes.
|
||||||
|
*/
|
||||||
|
WILDCARD_ROOT_NODES_ALLOWED,
|
||||||
|
/*
|
||||||
|
* Retrieve only explicitly mapped nodes, with wildcards
|
||||||
|
* allowed as leaf nodes.
|
||||||
|
*/
|
||||||
|
WILDCARD_LEAF_NODES_ALLOWED,
|
||||||
|
/*
|
||||||
|
* Retrieve both explicitly mapped and wildcard nodes.
|
||||||
|
*/
|
||||||
|
WILDCARD_NODES_ALLOWED
|
||||||
|
}
|
||||||
|
|
||||||
|
static EnumSet<TrieMatchingMode> EXPLICIT_OR_ROOT_WILDCARD =
|
||||||
|
EnumSet.of(TrieMatchingMode.EXPLICIT_NODES_ONLY, TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED);
|
||||||
|
|
||||||
public interface Decoder {
|
public interface Decoder {
|
||||||
String decode(String value);
|
String decode(String value);
|
||||||
}
|
}
|
||||||
|
@ -107,15 +142,15 @@ public class PathTrie<T> {
|
||||||
if (isNamedWildcard(token)) {
|
if (isNamedWildcard(token)) {
|
||||||
node.updateKeyWithNamedWildcard(token);
|
node.updateKeyWithNamedWildcard(token);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
// in case the target(last) node already exist but without a value
|
* If the target node already exists, but is without a value,
|
||||||
// than the value should be updated.
|
* then the value should be updated.
|
||||||
|
*/
|
||||||
if (index == (path.length - 1)) {
|
if (index == (path.length - 1)) {
|
||||||
if (node.value != null) {
|
if (node.value != null) {
|
||||||
throw new IllegalArgumentException("Path [" + String.join("/", path)+ "] already has a value ["
|
throw new IllegalArgumentException("Path [" + String.join("/", path)+ "] already has a value ["
|
||||||
+ node.value + "]");
|
+ node.value + "]");
|
||||||
}
|
} else {
|
||||||
if (node.value == null) {
|
|
||||||
node.value = value;
|
node.value = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,6 +159,40 @@ public class PathTrie<T> {
|
||||||
node.insert(path, index + 1, value);
|
node.insert(path, index + 1, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void insertOrUpdate(String[] path, int index, T value, BiFunction<T, T, T> updater) {
|
||||||
|
if (index >= path.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
String token = path[index];
|
||||||
|
String key = token;
|
||||||
|
if (isNamedWildcard(token)) {
|
||||||
|
key = wildcard;
|
||||||
|
}
|
||||||
|
TrieNode node = children.get(key);
|
||||||
|
if (node == null) {
|
||||||
|
T nodeValue = index == path.length - 1 ? value : null;
|
||||||
|
node = new TrieNode(token, nodeValue, wildcard);
|
||||||
|
addInnerChild(key, node);
|
||||||
|
} else {
|
||||||
|
if (isNamedWildcard(token)) {
|
||||||
|
node.updateKeyWithNamedWildcard(token);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* If the target node already exists, but is without a value,
|
||||||
|
* then the value should be updated.
|
||||||
|
*/
|
||||||
|
if (index == (path.length - 1)) {
|
||||||
|
if (node.value != null) {
|
||||||
|
node.value = updater.apply(node.value, value);
|
||||||
|
} else {
|
||||||
|
node.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.insertOrUpdate(path, index + 1, value, updater);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isNamedWildcard(String key) {
|
private boolean isNamedWildcard(String key) {
|
||||||
return key.indexOf('{') != -1 && key.indexOf('}') != -1;
|
return key.indexOf('{') != -1 && key.indexOf('}') != -1;
|
||||||
}
|
}
|
||||||
|
@ -136,23 +205,57 @@ public class PathTrie<T> {
|
||||||
return namedWildcard != null;
|
return namedWildcard != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T retrieve(String[] path, int index, Map<String, String> params) {
|
public T retrieve(String[] path, int index, Map<String, String> params, TrieMatchingMode trieMatchingMode) {
|
||||||
if (index >= path.length)
|
if (index >= path.length)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
String token = path[index];
|
String token = path[index];
|
||||||
TrieNode node = children.get(token);
|
TrieNode node = children.get(token);
|
||||||
boolean usedWildcard;
|
boolean usedWildcard;
|
||||||
|
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
|
if (trieMatchingMode == TrieMatchingMode.WILDCARD_NODES_ALLOWED) {
|
||||||
|
node = children.get(wildcard);
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
usedWildcard = true;
|
||||||
|
} else if (trieMatchingMode == TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED && index == 1) {
|
||||||
|
/*
|
||||||
|
* Allow root node wildcard matches.
|
||||||
|
*/
|
||||||
|
node = children.get(wildcard);
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
usedWildcard = true;
|
||||||
|
} else if (trieMatchingMode == TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED && index + 1 == path.length) {
|
||||||
|
/*
|
||||||
|
* Allow leaf node wildcard matches.
|
||||||
|
*/
|
||||||
node = children.get(wildcard);
|
node = children.get(wildcard);
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
usedWildcard = true;
|
usedWildcard = true;
|
||||||
} else {
|
} else {
|
||||||
// If we are at the end of the path, the current node does not have a value but there
|
return null;
|
||||||
// is a child wildcard node, use the child wildcard node
|
}
|
||||||
if (index + 1 == path.length && node.value == null && children.get(wildcard) != null) {
|
} else {
|
||||||
|
if (index + 1 == path.length && node.value == null && children.get(wildcard) != null
|
||||||
|
&& EXPLICIT_OR_ROOT_WILDCARD.contains(trieMatchingMode) == false) {
|
||||||
|
/*
|
||||||
|
* If we are at the end of the path, the current node does not have a value but
|
||||||
|
* there is a child wildcard node, use the child wildcard node.
|
||||||
|
*/
|
||||||
|
node = children.get(wildcard);
|
||||||
|
usedWildcard = true;
|
||||||
|
} else if (index == 1 && node.value == null && children.get(wildcard) != null
|
||||||
|
&& trieMatchingMode == TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED) {
|
||||||
|
/*
|
||||||
|
* If we are at the root, and root wildcards are allowed, use the child wildcard
|
||||||
|
* node.
|
||||||
|
*/
|
||||||
node = children.get(wildcard);
|
node = children.get(wildcard);
|
||||||
usedWildcard = true;
|
usedWildcard = true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -166,16 +269,16 @@ public class PathTrie<T> {
|
||||||
return node.value;
|
return node.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
T res = node.retrieve(path, index + 1, params);
|
T nodeValue = node.retrieve(path, index + 1, params, trieMatchingMode);
|
||||||
if (res == null && !usedWildcard) {
|
if (nodeValue == null && !usedWildcard && trieMatchingMode != TrieMatchingMode.EXPLICIT_NODES_ONLY) {
|
||||||
node = children.get(wildcard);
|
node = children.get(wildcard);
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
put(params, node, token);
|
put(params, node, token);
|
||||||
res = node.retrieve(path, index + 1, params);
|
nodeValue = node.retrieve(path, index + 1, params, trieMatchingMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return nodeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void put(Map<String, String> params, TrieNode node, String value) {
|
private void put(Map<String, String> params, TrieNode node, String value) {
|
||||||
|
@ -200,18 +303,47 @@ public class PathTrie<T> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int index = 0;
|
int index = 0;
|
||||||
// supports initial delimiter.
|
// Supports initial delimiter.
|
||||||
if (strings.length > 0 && strings[0].isEmpty()) {
|
if (strings.length > 0 && strings[0].isEmpty()) {
|
||||||
index = 1;
|
index = 1;
|
||||||
}
|
}
|
||||||
root.insert(strings, index, value);
|
root.insert(strings, index, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a value for the given path. If the path already exists, replace the value with:
|
||||||
|
* <pre>
|
||||||
|
* value = updater.apply(oldValue, newValue);
|
||||||
|
* </pre>
|
||||||
|
* allowing the value to be updated if desired.
|
||||||
|
*/
|
||||||
|
public void insertOrUpdate(String path, T value, BiFunction<T, T, T> updater) {
|
||||||
|
String[] strings = path.split(SEPARATOR);
|
||||||
|
if (strings.length == 0) {
|
||||||
|
if (rootValue != null) {
|
||||||
|
rootValue = updater.apply(rootValue, value);
|
||||||
|
} else {
|
||||||
|
rootValue = value;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int index = 0;
|
||||||
|
// Supports initial delimiter.
|
||||||
|
if (strings.length > 0 && strings[0].isEmpty()) {
|
||||||
|
index = 1;
|
||||||
|
}
|
||||||
|
root.insertOrUpdate(strings, index, value, updater);
|
||||||
|
}
|
||||||
|
|
||||||
public T retrieve(String path) {
|
public T retrieve(String path) {
|
||||||
return retrieve(path, null);
|
return retrieve(path, null, TrieMatchingMode.WILDCARD_NODES_ALLOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
public T retrieve(String path, Map<String, String> params) {
|
public T retrieve(String path, Map<String, String> params) {
|
||||||
|
return retrieve(path, params, TrieMatchingMode.WILDCARD_NODES_ALLOWED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T retrieve(String path, Map<String, String> params, TrieMatchingMode trieMatchingMode) {
|
||||||
if (path.length() == 0) {
|
if (path.length() == 0) {
|
||||||
return rootValue;
|
return rootValue;
|
||||||
}
|
}
|
||||||
|
@ -220,10 +352,56 @@ public class PathTrie<T> {
|
||||||
return rootValue;
|
return rootValue;
|
||||||
}
|
}
|
||||||
int index = 0;
|
int index = 0;
|
||||||
// supports initial delimiter.
|
|
||||||
|
// Supports initial delimiter.
|
||||||
if (strings.length > 0 && strings[0].isEmpty()) {
|
if (strings.length > 0 && strings[0].isEmpty()) {
|
||||||
index = 1;
|
index = 1;
|
||||||
}
|
}
|
||||||
return root.retrieve(strings, index, params);
|
|
||||||
|
return root.retrieve(strings, index, params, trieMatchingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator of the objects stored in the {@code PathTrie}, using
|
||||||
|
* all possible {@code TrieMatchingMode} modes. The {@code paramSupplier}
|
||||||
|
* is called between each invocation of {@code next()} to supply a new map
|
||||||
|
* of parameters.
|
||||||
|
*/
|
||||||
|
public Iterator<T> retrieveAll(String path, Supplier<Map<String, String>> paramSupplier) {
|
||||||
|
return new PathTrieIterator<>(this, path, paramSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PathTrieIterator<T> implements Iterator<T> {
|
||||||
|
|
||||||
|
private final List<TrieMatchingMode> modes;
|
||||||
|
private final Supplier<Map<String, String>> paramSupplier;
|
||||||
|
private final PathTrie<T> trie;
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
PathTrieIterator(PathTrie trie, String path, Supplier<Map<String, String>> paramSupplier) {
|
||||||
|
this.path = path;
|
||||||
|
this.trie = trie;
|
||||||
|
this.paramSupplier = paramSupplier;
|
||||||
|
this.modes = new ArrayList<>(Arrays.asList(TrieMatchingMode.EXPLICIT_NODES_ONLY,
|
||||||
|
TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED,
|
||||||
|
TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED,
|
||||||
|
TrieMatchingMode.WILDCARD_NODES_ALLOWED));
|
||||||
|
assert TrieMatchingMode.values().length == 4 : "missing trie matching mode";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return modes.isEmpty() == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T next() {
|
||||||
|
if (modes.isEmpty()) {
|
||||||
|
throw new NoSuchElementException("called next() without validating hasNext()! no more modes available");
|
||||||
|
}
|
||||||
|
TrieMatchingMode mode = modes.remove(0);
|
||||||
|
Map<String, String> params = paramSupplier.get();
|
||||||
|
return trie.retrieve(path, params, mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch licenses this file to you under
|
||||||
|
* the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.elasticsearch.rest;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulate multiple handlers for the same path, allowing different handlers for different HTTP verbs.
|
||||||
|
*/
|
||||||
|
final class MethodHandlers {
|
||||||
|
|
||||||
|
private final String path;
|
||||||
|
private final Map<RestRequest.Method, RestHandler> methodHandlers;
|
||||||
|
|
||||||
|
MethodHandlers(String path, RestHandler handler, RestRequest.Method... methods) {
|
||||||
|
this.path = path;
|
||||||
|
this.methodHandlers = new HashMap<>(methods.length);
|
||||||
|
for (RestRequest.Method method : methods) {
|
||||||
|
methodHandlers.put(method, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an additional method and handler for an existing path. Note that {@code MethodHandlers}
|
||||||
|
* does not allow replacing the handler for an already existing method.
|
||||||
|
*/
|
||||||
|
public MethodHandlers addMethod(RestRequest.Method method, RestHandler handler) {
|
||||||
|
RestHandler existing = methodHandlers.putIfAbsent(method, handler);
|
||||||
|
if (existing != null) {
|
||||||
|
throw new IllegalArgumentException("Cannot replace existing handler for [" + path + "] for method: " + method);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a handler for an additional array of methods. Note that {@code MethodHandlers}
|
||||||
|
* does not allow replacing the handler for an already existing method.
|
||||||
|
*/
|
||||||
|
public MethodHandlers addMethods(RestHandler handler, RestRequest.Method... methods) {
|
||||||
|
for (RestRequest.Method method : methods) {
|
||||||
|
addMethod(method, handler);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an Optional-wrapped handler for a method, or an empty optional if
|
||||||
|
* there is no handler.
|
||||||
|
*/
|
||||||
|
public Optional<RestHandler> getHandler(RestRequest.Method method) {
|
||||||
|
return Optional.ofNullable(methodHandlers.get(method));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a set of all valid HTTP methods for the particular path
|
||||||
|
*/
|
||||||
|
public Set<RestRequest.Method> getValidMethods() {
|
||||||
|
return methodHandlers.keySet();
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,9 +42,14 @@ import org.elasticsearch.usage.UsageService;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
@ -58,12 +63,7 @@ import static org.elasticsearch.rest.RestStatus.OK;
|
||||||
|
|
||||||
public class RestController extends AbstractComponent implements HttpServerTransport.Dispatcher {
|
public class RestController extends AbstractComponent implements HttpServerTransport.Dispatcher {
|
||||||
|
|
||||||
private final PathTrie<RestHandler> getHandlers = new PathTrie<>(RestUtils.REST_DECODER);
|
private final PathTrie<MethodHandlers> handlers = new PathTrie<>(RestUtils.REST_DECODER);
|
||||||
private final PathTrie<RestHandler> postHandlers = new PathTrie<>(RestUtils.REST_DECODER);
|
|
||||||
private final PathTrie<RestHandler> putHandlers = new PathTrie<>(RestUtils.REST_DECODER);
|
|
||||||
private final PathTrie<RestHandler> deleteHandlers = new PathTrie<>(RestUtils.REST_DECODER);
|
|
||||||
private final PathTrie<RestHandler> headHandlers = new PathTrie<>(RestUtils.REST_DECODER);
|
|
||||||
private final PathTrie<RestHandler> optionsHandlers = new PathTrie<>(RestUtils.REST_DECODER);
|
|
||||||
|
|
||||||
private final UnaryOperator<RestHandler> handlerWrapper;
|
private final UnaryOperator<RestHandler> handlerWrapper;
|
||||||
|
|
||||||
|
@ -148,24 +148,19 @@ public class RestController extends AbstractComponent implements HttpServerTrans
|
||||||
* @param handler The handler to actually execute
|
* @param handler The handler to actually execute
|
||||||
*/
|
*/
|
||||||
public void registerHandler(RestRequest.Method method, String path, RestHandler handler) {
|
public void registerHandler(RestRequest.Method method, String path, RestHandler handler) {
|
||||||
PathTrie<RestHandler> handlers = getHandlersForMethod(method);
|
|
||||||
if (handlers != null) {
|
|
||||||
handlers.insert(path, handler);
|
|
||||||
if (handler instanceof BaseRestHandler) {
|
if (handler instanceof BaseRestHandler) {
|
||||||
usageService.addRestHandler((BaseRestHandler) handler);
|
usageService.addRestHandler((BaseRestHandler) handler);
|
||||||
}
|
}
|
||||||
} else {
|
handlers.insertOrUpdate(path, new MethodHandlers(path, handler, method), (mHandlers, newMHandler) -> {
|
||||||
throw new IllegalArgumentException("Can't handle [" + method + "] for path [" + path + "]");
|
return mHandlers.addMethods(handler, method);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param request The current request. Must not be null.
|
|
||||||
* @return true iff the circuit breaker limit must be enforced for processing this request.
|
* @return true iff the circuit breaker limit must be enforced for processing this request.
|
||||||
*/
|
*/
|
||||||
public boolean canTripCircuitBreaker(RestRequest request) {
|
public boolean canTripCircuitBreaker(final Optional<RestHandler> handler) {
|
||||||
RestHandler handler = getHandler(request);
|
return handler.map(h -> h.canTripCircuitBreaker()).orElse(true);
|
||||||
return (handler != null) ? handler.canTripCircuitBreaker() : true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -174,32 +169,11 @@ public class RestController extends AbstractComponent implements HttpServerTrans
|
||||||
handleFavicon(request, channel);
|
handleFavicon(request, channel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RestChannel responseChannel = channel;
|
|
||||||
try {
|
try {
|
||||||
final int contentLength = request.hasContent() ? request.content().length() : 0;
|
tryAllHandlers(request, channel, threadContext);
|
||||||
assert contentLength >= 0 : "content length was negative, how is that possible?";
|
|
||||||
final RestHandler handler = getHandler(request);
|
|
||||||
|
|
||||||
if (contentLength > 0 && hasContentType(request, handler) == false) {
|
|
||||||
sendContentTypeErrorMessage(request, responseChannel);
|
|
||||||
} else if (contentLength > 0 && handler != null && handler.supportsContentStream() &&
|
|
||||||
request.getXContentType() != XContentType.JSON && request.getXContentType() != XContentType.SMILE) {
|
|
||||||
responseChannel.sendResponse(BytesRestResponse.createSimpleErrorResponse(responseChannel,
|
|
||||||
RestStatus.NOT_ACCEPTABLE, "Content-Type [" + request.getXContentType() +
|
|
||||||
"] does not support stream parsing. Use JSON or SMILE instead"));
|
|
||||||
} else {
|
|
||||||
if (canTripCircuitBreaker(request)) {
|
|
||||||
inFlightRequestsBreaker(circuitBreakerService).addEstimateBytesAndMaybeBreak(contentLength, "<http_request>");
|
|
||||||
} else {
|
|
||||||
inFlightRequestsBreaker(circuitBreakerService).addWithoutBreaking(contentLength);
|
|
||||||
}
|
|
||||||
// iff we could reserve bytes for the request we need to send the response also over this channel
|
|
||||||
responseChannel = new ResourceHandlingHttpChannel(channel, circuitBreakerService, contentLength);
|
|
||||||
dispatchRequest(request, responseChannel, client, threadContext, handler);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
try {
|
try {
|
||||||
responseChannel.sendResponse(new BytesRestResponse(channel, e));
|
channel.sendResponse(new BytesRestResponse(channel, e));
|
||||||
} catch (Exception inner) {
|
} catch (Exception inner) {
|
||||||
inner.addSuppressed(e);
|
inner.addSuppressed(e);
|
||||||
logger.error((Supplier<?>) () ->
|
logger.error((Supplier<?>) () ->
|
||||||
|
@ -233,33 +207,56 @@ public class RestController extends AbstractComponent implements HttpServerTrans
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispatchRequest(final RestRequest request, final RestChannel channel, final NodeClient client, ThreadContext threadContext,
|
/**
|
||||||
final RestHandler handler) throws Exception {
|
* Dispatch the request, if possible, returning true if a response was sent or false otherwise.
|
||||||
if (checkRequestParameters(request, channel) == false) {
|
*/
|
||||||
channel
|
boolean dispatchRequest(final RestRequest request, final RestChannel channel, final NodeClient client,
|
||||||
.sendResponse(BytesRestResponse.createSimpleErrorResponse(channel,BAD_REQUEST, "error traces in responses are disabled."));
|
ThreadContext threadContext, final Optional<RestHandler> mHandler) throws Exception {
|
||||||
} else {
|
final int contentLength = request.hasContent() ? request.content().length() : 0;
|
||||||
for (String key : headersToCopy) {
|
|
||||||
String httpHeader = request.header(key);
|
|
||||||
if (httpHeader != null) {
|
|
||||||
threadContext.putHeader(key, httpHeader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handler == null) {
|
RestChannel responseChannel = channel;
|
||||||
|
// Indicator of whether a response was sent or not
|
||||||
|
boolean requestHandled;
|
||||||
|
|
||||||
|
if (contentLength > 0 && mHandler.map(h -> hasContentType(request, h) == false).orElse(false)) {
|
||||||
|
sendContentTypeErrorMessage(request, channel);
|
||||||
|
requestHandled = true;
|
||||||
|
} else if (contentLength > 0 && mHandler.map(h -> h.supportsContentStream()).orElse(false) &&
|
||||||
|
request.getXContentType() != XContentType.JSON && request.getXContentType() != XContentType.SMILE) {
|
||||||
|
channel.sendResponse(BytesRestResponse.createSimpleErrorResponse(channel,
|
||||||
|
RestStatus.NOT_ACCEPTABLE, "Content-Type [" + request.getXContentType() +
|
||||||
|
"] does not support stream parsing. Use JSON or SMILE instead"));
|
||||||
|
requestHandled = true;
|
||||||
|
} else if (mHandler.isPresent()) {
|
||||||
|
try {
|
||||||
|
if (canTripCircuitBreaker(mHandler)) {
|
||||||
|
inFlightRequestsBreaker(circuitBreakerService).addEstimateBytesAndMaybeBreak(contentLength, "<http_request>");
|
||||||
|
} else {
|
||||||
|
inFlightRequestsBreaker(circuitBreakerService).addWithoutBreaking(contentLength);
|
||||||
|
}
|
||||||
|
// iff we could reserve bytes for the request we need to send the response also over this channel
|
||||||
|
responseChannel = new ResourceHandlingHttpChannel(channel, circuitBreakerService, contentLength);
|
||||||
|
|
||||||
|
final RestHandler wrappedHandler = mHandler.map(h -> handlerWrapper.apply(h)).get();
|
||||||
|
wrappedHandler.handleRequest(request, responseChannel, client);
|
||||||
|
requestHandled = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
responseChannel.sendResponse(new BytesRestResponse(responseChannel, e));
|
||||||
|
// We "handled" the request by returning a response, even though it was an error
|
||||||
|
requestHandled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (request.method() == RestRequest.Method.OPTIONS) {
|
if (request.method() == RestRequest.Method.OPTIONS) {
|
||||||
// when we have OPTIONS request, simply send OK by default (with the Access Control Origin header which gets automatically added)
|
// when we have OPTIONS request, simply send OK by default (with the Access Control Origin header which gets automatically added)
|
||||||
|
|
||||||
channel.sendResponse(new BytesRestResponse(OK, BytesRestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY));
|
channel.sendResponse(new BytesRestResponse(OK, BytesRestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY));
|
||||||
|
requestHandled = true;
|
||||||
} else {
|
} else {
|
||||||
final String msg = "No handler found for uri [" + request.uri() + "] and method [" + request.method() + "]";
|
requestHandled = false;
|
||||||
channel.sendResponse(new BytesRestResponse(BAD_REQUEST, msg));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final RestHandler wrappedHandler = Objects.requireNonNull(handlerWrapper.apply(handler));
|
|
||||||
wrappedHandler.handleRequest(request, channel, client);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Return true if the request was handled, false otherwise.
|
||||||
|
return requestHandled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -308,34 +305,71 @@ public class RestController extends AbstractComponent implements HttpServerTrans
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private RestHandler getHandler(RestRequest request) {
|
void tryAllHandlers(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) throws Exception {
|
||||||
String path = getPath(request);
|
for (String key : headersToCopy) {
|
||||||
PathTrie<RestHandler> handlers = getHandlersForMethod(request.method());
|
String httpHeader = request.header(key);
|
||||||
if (handlers != null) {
|
if (httpHeader != null) {
|
||||||
return handlers.retrieve(path, request.params());
|
threadContext.putHeader(key, httpHeader);
|
||||||
} else {
|
}
|
||||||
return null;
|
}
|
||||||
|
// Request execution flag
|
||||||
|
boolean requestHandled = false;
|
||||||
|
|
||||||
|
if (checkRequestParameters(request, channel) == false) {
|
||||||
|
channel.sendResponse(BytesRestResponse.createSimpleErrorResponse(channel,
|
||||||
|
BAD_REQUEST, "error traces in responses are disabled."));
|
||||||
|
requestHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through all possible handlers, attempting to dispatch the request
|
||||||
|
Iterator<MethodHandlers> allHandlers = getAllHandlers(request);
|
||||||
|
for (Iterator<MethodHandlers> it = allHandlers; it.hasNext(); ) {
|
||||||
|
final Optional<RestHandler> mHandler = Optional.ofNullable(it.next()).flatMap(mh -> mh.getHandler(request.method()));
|
||||||
|
requestHandled = dispatchRequest(request, channel, client, threadContext, mHandler);
|
||||||
|
if (requestHandled) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PathTrie<RestHandler> getHandlersForMethod(RestRequest.Method method) {
|
// If request has not been handled, fallback to a bad request error.
|
||||||
if (method == RestRequest.Method.GET) {
|
if (requestHandled == false) {
|
||||||
return getHandlers;
|
handleBadRequest(request, channel);
|
||||||
} else if (method == RestRequest.Method.POST) {
|
|
||||||
return postHandlers;
|
|
||||||
} else if (method == RestRequest.Method.PUT) {
|
|
||||||
return putHandlers;
|
|
||||||
} else if (method == RestRequest.Method.DELETE) {
|
|
||||||
return deleteHandlers;
|
|
||||||
} else if (method == RestRequest.Method.HEAD) {
|
|
||||||
return headHandlers;
|
|
||||||
} else if (method == RestRequest.Method.OPTIONS) {
|
|
||||||
return optionsHandlers;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Iterator<MethodHandlers> getAllHandlers(final RestRequest request) {
|
||||||
|
// Between retrieving the correct path, we need to reset the parameters,
|
||||||
|
// otherwise parameters are parsed out of the URI that aren't actually handled.
|
||||||
|
final Map<String, String> originalParams = new HashMap<>(request.params());
|
||||||
|
return handlers.retrieveAll(getPath(request), () -> {
|
||||||
|
// PathTrie modifies the request, so reset the params between each iteration
|
||||||
|
request.params().clear();
|
||||||
|
request.params().putAll(originalParams);
|
||||||
|
return request.params();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a requests with no candidate handlers (return a 400 Bad Request
|
||||||
|
* error).
|
||||||
|
*/
|
||||||
|
private void handleBadRequest(RestRequest request, RestChannel channel) {
|
||||||
|
channel.sendResponse(new BytesRestResponse(BAD_REQUEST,
|
||||||
|
"No handler found for uri [" + request.uri() + "] and method [" + request.method() + "]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the valid set of HTTP methods for a REST request.
|
||||||
|
*/
|
||||||
|
private Set<RestRequest.Method> getValidHandlerMethodSet(RestRequest request) {
|
||||||
|
Set<RestRequest.Method> validMethods = new HashSet<>();
|
||||||
|
Iterator<MethodHandlers> allHandlers = getAllHandlers(request);
|
||||||
|
for (Iterator<MethodHandlers> it = allHandlers; it.hasNext(); ) {
|
||||||
|
Optional.ofNullable(it.next()).map(mh -> validMethods.addAll(mh.getValidMethods()));
|
||||||
|
}
|
||||||
|
return validMethods;
|
||||||
|
}
|
||||||
|
|
||||||
private String getPath(RestRequest request) {
|
private String getPath(RestRequest request) {
|
||||||
// we use rawPath since we don't want to decode it while processing the path resolution
|
// we use rawPath since we don't want to decode it while processing the path resolution
|
||||||
// so we can handle things like:
|
// so we can handle things like:
|
||||||
|
|
|
@ -36,7 +36,7 @@ import static org.elasticsearch.rest.RestRequest.Method.GET;
|
||||||
public class RestGetTaskAction extends BaseRestHandler {
|
public class RestGetTaskAction extends BaseRestHandler {
|
||||||
public RestGetTaskAction(Settings settings, RestController controller) {
|
public RestGetTaskAction(Settings settings, RestController controller) {
|
||||||
super(settings);
|
super(settings);
|
||||||
controller.registerHandler(GET, "/_tasks/{taskId}", this);
|
controller.registerHandler(GET, "/_tasks/{task_id}", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -46,7 +46,7 @@ public class RestGetTaskAction extends BaseRestHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
|
public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
|
||||||
TaskId taskId = new TaskId(request.param("taskId"));
|
TaskId taskId = new TaskId(request.param("task_id"));
|
||||||
boolean waitForCompletion = request.paramAsBoolean("wait_for_completion", false);
|
boolean waitForCompletion = request.paramAsBoolean("wait_for_completion", false);
|
||||||
TimeValue timeout = request.paramAsTime("timeout", null);
|
TimeValue timeout = request.paramAsTime("timeout", null);
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ public class ActionModuleTests extends ESTestCase {
|
||||||
// At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail
|
// At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail
|
||||||
Exception e = expectThrows(IllegalArgumentException.class, () ->
|
Exception e = expectThrows(IllegalArgumentException.class, () ->
|
||||||
actionModule.getRestController().registerHandler(Method.GET, "/", null));
|
actionModule.getRestController().registerHandler(Method.GET, "/", null));
|
||||||
assertThat(e.getMessage(), startsWith("Path [/] already has a value [" + RestMainAction.class.getName()));
|
assertThat(e.getMessage(), startsWith("Cannot replace existing handler for [/] for method: GET"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testPluginCantOverwriteBuiltinRestHandler() throws IOException {
|
public void testPluginCantOverwriteBuiltinRestHandler() throws IOException {
|
||||||
|
@ -139,7 +139,7 @@ public class ActionModuleTests extends ESTestCase {
|
||||||
settings.getIndexScopedSettings(), settings.getClusterSettings(), settings.getSettingsFilter(), threadPool,
|
settings.getIndexScopedSettings(), settings.getClusterSettings(), settings.getSettingsFilter(), threadPool,
|
||||||
singletonList(dupsMainAction), null, null, usageService);
|
singletonList(dupsMainAction), null, null, usageService);
|
||||||
Exception e = expectThrows(IllegalArgumentException.class, () -> actionModule.initRestHandlers(null));
|
Exception e = expectThrows(IllegalArgumentException.class, () -> actionModule.initRestHandlers(null));
|
||||||
assertThat(e.getMessage(), startsWith("Path [/] already has a value [" + RestMainAction.class.getName()));
|
assertThat(e.getMessage(), startsWith("Cannot replace existing handler for [/] for method: GET"));
|
||||||
} finally {
|
} finally {
|
||||||
threadPool.shutdown();
|
threadPool.shutdown();
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@ public class ActionModuleTests extends ESTestCase {
|
||||||
// At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail
|
// At this point the easiest way to confirm that a handler is loaded is to try to register another one on top of it and to fail
|
||||||
Exception e = expectThrows(IllegalArgumentException.class, () ->
|
Exception e = expectThrows(IllegalArgumentException.class, () ->
|
||||||
actionModule.getRestController().registerHandler(Method.GET, "/_dummy", null));
|
actionModule.getRestController().registerHandler(Method.GET, "/_dummy", null));
|
||||||
assertThat(e.getMessage(), startsWith("Path [/_dummy] already has a value [" + FakeHandler.class.getName()));
|
assertThat(e.getMessage(), startsWith("Cannot replace existing handler for [/_dummy] for method: GET"));
|
||||||
} finally {
|
} finally {
|
||||||
threadPool.shutdown();
|
threadPool.shutdown();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,14 @@ import org.elasticsearch.rest.RestUtils;
|
||||||
import org.elasticsearch.test.ESTestCase;
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.path.PathTrie.TrieMatchingMode;
|
||||||
|
|
||||||
public class PathTrieTests extends ESTestCase {
|
public class PathTrieTests extends ESTestCase {
|
||||||
|
|
||||||
public static final PathTrie.Decoder NO_DECODER = new PathTrie.Decoder() {
|
public static final PathTrie.Decoder NO_DECODER = new PathTrie.Decoder() {
|
||||||
|
@ -114,6 +117,102 @@ public class PathTrieTests extends ESTestCase {
|
||||||
assertThat(trie.retrieve("/v/x/c", params), equalTo("test6"));
|
assertThat(trie.retrieve("/v/x/c", params), equalTo("test6"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/elastic/elasticsearch/pull/17916
|
||||||
|
public void testWildcardMatchingModes() {
|
||||||
|
PathTrie<String> trie = new PathTrie<>(NO_DECODER);
|
||||||
|
trie.insert("{testA}", "test1");
|
||||||
|
trie.insert("{testA}/{testB}", "test2");
|
||||||
|
trie.insert("a/{testA}", "test3");
|
||||||
|
trie.insert("{testA}/b", "test4");
|
||||||
|
trie.insert("{testA}/b/c", "test5");
|
||||||
|
trie.insert("a/{testB}/c", "test6");
|
||||||
|
trie.insert("a/b/{testC}", "test7");
|
||||||
|
trie.insert("{testA}/b/{testB}", "test8");
|
||||||
|
trie.insert("x/{testA}/z", "test9");
|
||||||
|
trie.insert("{testA}/{testB}/{testC}", "test10");
|
||||||
|
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
|
||||||
|
assertThat(trie.retrieve("/a", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), nullValue());
|
||||||
|
assertThat(trie.retrieve("/a", params, TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED), equalTo("test1"));
|
||||||
|
assertThat(trie.retrieve("/a", params, TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED), equalTo("test1"));
|
||||||
|
assertThat(trie.retrieve("/a", params, TrieMatchingMode.WILDCARD_NODES_ALLOWED), equalTo("test1"));
|
||||||
|
Iterator<String> allPaths = trie.retrieveAll("/a", () -> params);
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo("test1"));
|
||||||
|
assertThat(allPaths.next(), equalTo("test1"));
|
||||||
|
assertThat(allPaths.next(), equalTo("test1"));
|
||||||
|
assertFalse(allPaths.hasNext());
|
||||||
|
|
||||||
|
assertThat(trie.retrieve("/a/b", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), nullValue());
|
||||||
|
assertThat(trie.retrieve("/a/b", params, TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED), equalTo("test4"));
|
||||||
|
assertThat(trie.retrieve("/a/b", params, TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED), equalTo("test3"));
|
||||||
|
assertThat(trie.retrieve("/a/b", params, TrieMatchingMode.WILDCARD_NODES_ALLOWED), equalTo("test3"));
|
||||||
|
allPaths = trie.retrieveAll("/a/b", () -> params);
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo("test4"));
|
||||||
|
assertThat(allPaths.next(), equalTo("test3"));
|
||||||
|
assertThat(allPaths.next(), equalTo("test3"));
|
||||||
|
assertFalse(allPaths.hasNext());
|
||||||
|
|
||||||
|
assertThat(trie.retrieve("/a/b/c", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), nullValue());
|
||||||
|
assertThat(trie.retrieve("/a/b/c", params, TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED), equalTo("test5"));
|
||||||
|
assertThat(trie.retrieve("/a/b/c", params, TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED), equalTo("test7"));
|
||||||
|
assertThat(trie.retrieve("/a/b/c", params, TrieMatchingMode.WILDCARD_NODES_ALLOWED), equalTo("test7"));
|
||||||
|
allPaths = trie.retrieveAll("/a/b/c", () -> params);
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo("test5"));
|
||||||
|
assertThat(allPaths.next(), equalTo("test7"));
|
||||||
|
assertThat(allPaths.next(), equalTo("test7"));
|
||||||
|
assertFalse(allPaths.hasNext());
|
||||||
|
|
||||||
|
assertThat(trie.retrieve("/x/y/z", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), nullValue());
|
||||||
|
assertThat(trie.retrieve("/x/y/z", params, TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED), nullValue());
|
||||||
|
assertThat(trie.retrieve("/x/y/z", params, TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED), nullValue());
|
||||||
|
assertThat(trie.retrieve("/x/y/z", params, TrieMatchingMode.WILDCARD_NODES_ALLOWED), equalTo("test9"));
|
||||||
|
allPaths = trie.retrieveAll("/x/y/z", () -> params);
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo("test9"));
|
||||||
|
assertFalse(allPaths.hasNext());
|
||||||
|
|
||||||
|
assertThat(trie.retrieve("/d/e/f", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), nullValue());
|
||||||
|
assertThat(trie.retrieve("/d/e/f", params, TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED), nullValue());
|
||||||
|
assertThat(trie.retrieve("/d/e/f", params, TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED), nullValue());
|
||||||
|
assertThat(trie.retrieve("/d/e/f", params, TrieMatchingMode.WILDCARD_NODES_ALLOWED), equalTo("test10"));
|
||||||
|
allPaths = trie.retrieveAll("/d/e/f", () -> params);
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo(null));
|
||||||
|
assertThat(allPaths.next(), equalTo("test10"));
|
||||||
|
assertFalse(allPaths.hasNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/elastic/elasticsearch/pull/17916
|
||||||
|
public void testExplicitMatchingMode() {
|
||||||
|
PathTrie<String> trie = new PathTrie<>(NO_DECODER);
|
||||||
|
trie.insert("{testA}", "test1");
|
||||||
|
trie.insert("a", "test2");
|
||||||
|
trie.insert("{testA}/{testB}", "test3");
|
||||||
|
trie.insert("a/{testB}", "test4");
|
||||||
|
trie.insert("{testB}/b", "test5");
|
||||||
|
trie.insert("a/b", "test6");
|
||||||
|
trie.insert("{testA}/b/{testB}", "test7");
|
||||||
|
trie.insert("x/{testA}/z", "test8");
|
||||||
|
trie.insert("{testA}/{testB}/{testC}", "test9");
|
||||||
|
trie.insert("a/b/c", "test10");
|
||||||
|
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
|
||||||
|
assertThat(trie.retrieve("/a", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), equalTo("test2"));
|
||||||
|
assertThat(trie.retrieve("/x", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), nullValue());
|
||||||
|
assertThat(trie.retrieve("/a/b", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), equalTo("test6"));
|
||||||
|
assertThat(trie.retrieve("/a/x", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), nullValue());
|
||||||
|
assertThat(trie.retrieve("/a/b/c", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), equalTo("test10"));
|
||||||
|
assertThat(trie.retrieve("/x/y/z", params, TrieMatchingMode.EXPLICIT_NODES_ONLY), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
public void testSamePathConcreteResolution() {
|
public void testSamePathConcreteResolution() {
|
||||||
PathTrie<String> trie = new PathTrie<>(NO_DECODER);
|
PathTrie<String> trie = new PathTrie<>(NO_DECODER);
|
||||||
trie.insert("{x}/{y}/{z}", "test1");
|
trie.insert("{x}/{y}/{z}", "test1");
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.elasticsearch.common.bytes.BytesArray;
|
||||||
import org.elasticsearch.common.bytes.BytesReference;
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
import org.elasticsearch.common.component.AbstractLifecycleComponent;
|
import org.elasticsearch.common.component.AbstractLifecycleComponent;
|
||||||
import org.elasticsearch.common.logging.DeprecationLogger;
|
import org.elasticsearch.common.logging.DeprecationLogger;
|
||||||
|
import org.elasticsearch.common.path.PathTrie;
|
||||||
import org.elasticsearch.common.settings.ClusterSettings;
|
import org.elasticsearch.common.settings.ClusterSettings;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.common.transport.BoundTransportAddress;
|
import org.elasticsearch.common.transport.BoundTransportAddress;
|
||||||
|
@ -49,8 +50,10 @@ import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
@ -61,7 +64,9 @@ import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.doCallRealMethod;
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class RestControllerTests extends ESTestCase {
|
public class RestControllerTests extends ESTestCase {
|
||||||
|
|
||||||
|
@ -95,7 +100,6 @@ public class RestControllerTests extends ESTestCase {
|
||||||
httpServerTransport.start();
|
httpServerTransport.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void testApplyRelevantHeaders() throws Exception {
|
public void testApplyRelevantHeaders() throws Exception {
|
||||||
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||||
Set<String> headers = new HashSet<>(Arrays.asList("header.1", "header.2"));
|
Set<String> headers = new HashSet<>(Arrays.asList("header.1", "header.2"));
|
||||||
|
@ -104,12 +108,26 @@ public class RestControllerTests extends ESTestCase {
|
||||||
restHeaders.put("header.1", Collections.singletonList("true"));
|
restHeaders.put("header.1", Collections.singletonList("true"));
|
||||||
restHeaders.put("header.2", Collections.singletonList("true"));
|
restHeaders.put("header.2", Collections.singletonList("true"));
|
||||||
restHeaders.put("header.3", Collections.singletonList("false"));
|
restHeaders.put("header.3", Collections.singletonList("false"));
|
||||||
restController.dispatchRequest(new FakeRestRequest.Builder(xContentRegistry()).withHeaders(restHeaders).build(), null, null,
|
RestRequest fakeRequest = new FakeRestRequest.Builder(xContentRegistry()).withHeaders(restHeaders).build();
|
||||||
threadContext, (RestRequest request, RestChannel channel, NodeClient client) -> {
|
final RestController spyRestController = spy(restController);
|
||||||
|
when(spyRestController.getAllHandlers(fakeRequest))
|
||||||
|
.thenReturn(new Iterator<MethodHandlers>() {
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MethodHandlers next() {
|
||||||
|
return new MethodHandlers("/", (RestRequest request, RestChannel channel, NodeClient client) -> {
|
||||||
assertEquals("true", threadContext.getHeader("header.1"));
|
assertEquals("true", threadContext.getHeader("header.1"));
|
||||||
assertEquals("true", threadContext.getHeader("header.2"));
|
assertEquals("true", threadContext.getHeader("header.2"));
|
||||||
assertNull(threadContext.getHeader("header.3"));
|
assertNull(threadContext.getHeader("header.3"));
|
||||||
|
}, RestRequest.Method.GET);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
AssertingChannel channel = new AssertingChannel(fakeRequest, false, RestStatus.BAD_REQUEST);
|
||||||
|
restController.dispatchRequest(fakeRequest, channel, threadContext);
|
||||||
// the rest controller relies on the caller to stash the context, so we should expect these values here as we didn't stash the
|
// the rest controller relies on the caller to stash the context, so we should expect these values here as we didn't stash the
|
||||||
// context in this test
|
// context in this test
|
||||||
assertEquals("true", threadContext.getHeader("header.1"));
|
assertEquals("true", threadContext.getHeader("header.1"));
|
||||||
|
@ -124,10 +142,22 @@ public class RestControllerTests extends ESTestCase {
|
||||||
controller.registerHandler(RestRequest.Method.GET, "/trip", new FakeRestHandler(true));
|
controller.registerHandler(RestRequest.Method.GET, "/trip", new FakeRestHandler(true));
|
||||||
controller.registerHandler(RestRequest.Method.GET, "/do-not-trip", new FakeRestHandler(false));
|
controller.registerHandler(RestRequest.Method.GET, "/do-not-trip", new FakeRestHandler(false));
|
||||||
|
|
||||||
assertTrue(controller.canTripCircuitBreaker(new FakeRestRequest.Builder(xContentRegistry()).withPath("/trip").build()));
|
RestRequest fakeRequest = new FakeRestRequest.Builder(xContentRegistry()).withPath("/trip").build();
|
||||||
|
for (Iterator<MethodHandlers> it = controller.getAllHandlers(fakeRequest); it.hasNext(); ) {
|
||||||
|
Optional<MethodHandlers> mHandler = Optional.ofNullable(it.next());
|
||||||
|
assertTrue(mHandler.map(mh -> controller.canTripCircuitBreaker(mh.getHandler(RestRequest.Method.GET))).orElse(true));
|
||||||
|
}
|
||||||
// assume trip even on unknown paths
|
// assume trip even on unknown paths
|
||||||
assertTrue(controller.canTripCircuitBreaker(new FakeRestRequest.Builder(xContentRegistry()).withPath("/unknown-path").build()));
|
fakeRequest = new FakeRestRequest.Builder(xContentRegistry()).withPath("/unknown-path").build();
|
||||||
assertFalse(controller.canTripCircuitBreaker(new FakeRestRequest.Builder(xContentRegistry()).withPath("/do-not-trip").build()));
|
for (Iterator<MethodHandlers> it = controller.getAllHandlers(fakeRequest); it.hasNext(); ) {
|
||||||
|
Optional<MethodHandlers> mHandler = Optional.ofNullable(it.next());
|
||||||
|
assertTrue(mHandler.map(mh -> controller.canTripCircuitBreaker(mh.getHandler(RestRequest.Method.GET))).orElse(true));
|
||||||
|
}
|
||||||
|
fakeRequest = new FakeRestRequest.Builder(xContentRegistry()).withPath("/do-not-trip").build();
|
||||||
|
for (Iterator<MethodHandlers> it = controller.getAllHandlers(fakeRequest); it.hasNext(); ) {
|
||||||
|
Optional<MethodHandlers> mHandler = Optional.ofNullable(it.next());
|
||||||
|
assertFalse(mHandler.map(mh -> controller.canTripCircuitBreaker(mh.getHandler(RestRequest.Method.GET))).orElse(false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testRegisterAsDeprecatedHandler() {
|
public void testRegisterAsDeprecatedHandler() {
|
||||||
|
@ -182,7 +212,8 @@ public class RestControllerTests extends ESTestCase {
|
||||||
final RestController restController = new RestController(Settings.EMPTY, Collections.emptySet(), wrapper, null,
|
final RestController restController = new RestController(Settings.EMPTY, Collections.emptySet(), wrapper, null,
|
||||||
circuitBreakerService, usageService);
|
circuitBreakerService, usageService);
|
||||||
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
final ThreadContext threadContext = new ThreadContext(Settings.EMPTY);
|
||||||
restController.dispatchRequest(new FakeRestRequest.Builder(xContentRegistry()).build(), null, null, threadContext, handler);
|
restController.dispatchRequest(new FakeRestRequest.Builder(xContentRegistry()).build(),
|
||||||
|
null, null, threadContext, Optional.of(handler));
|
||||||
assertTrue(wrapperCalled.get());
|
assertTrue(wrapperCalled.get());
|
||||||
assertFalse(handlerCalled.get());
|
assertFalse(handlerCalled.get());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue