Skip to content

Commit 30b5ca7

Browse files
authored
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
1 parent 6e5cc42 commit 30b5ca7

File tree

7 files changed

+543
-121
lines changed

7 files changed

+543
-121
lines changed

core/src/main/java/org/elasticsearch/common/path/PathTrie.java

Lines changed: 198 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,49 @@
1919

2020
package org.elasticsearch.common.path;
2121

22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.EnumSet;
2225
import java.util.HashMap;
26+
import java.util.HashSet;
27+
import java.util.Iterator;
28+
import java.util.List;
2329
import java.util.Map;
30+
import java.util.NoSuchElementException;
31+
import java.util.Set;
32+
import java.util.function.BiFunction;
33+
import java.util.function.Supplier;
2434

2535
import static java.util.Collections.emptyMap;
2636
import static java.util.Collections.unmodifiableMap;
2737

2838
public class PathTrie<T> {
2939

40+
enum TrieMatchingMode {
41+
/*
42+
* Retrieve only explicitly mapped nodes, no wildcards are
43+
* matched.
44+
*/
45+
EXPLICIT_NODES_ONLY,
46+
/*
47+
* Retrieve only explicitly mapped nodes, with wildcards
48+
* allowed as root nodes.
49+
*/
50+
WILDCARD_ROOT_NODES_ALLOWED,
51+
/*
52+
* Retrieve only explicitly mapped nodes, with wildcards
53+
* allowed as leaf nodes.
54+
*/
55+
WILDCARD_LEAF_NODES_ALLOWED,
56+
/*
57+
* Retrieve both explicitly mapped and wildcard nodes.
58+
*/
59+
WILDCARD_NODES_ALLOWED
60+
}
61+
62+
static EnumSet<TrieMatchingMode> EXPLICIT_OR_ROOT_WILDCARD =
63+
EnumSet.of(TrieMatchingMode.EXPLICIT_NODES_ONLY, TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED);
64+
3065
public interface Decoder {
3166
String decode(String value);
3267
}
@@ -107,15 +142,15 @@ public synchronized void insert(String[] path, int index, T value) {
107142
if (isNamedWildcard(token)) {
108143
node.updateKeyWithNamedWildcard(token);
109144
}
110-
111-
// in case the target(last) node already exist but without a value
112-
// than the value should be updated.
145+
/*
146+
* If the target node already exists, but is without a value,
147+
* then the value should be updated.
148+
*/
113149
if (index == (path.length - 1)) {
114150
if (node.value != null) {
115151
throw new IllegalArgumentException("Path [" + String.join("/", path)+ "] already has a value ["
116152
+ node.value + "]");
117-
}
118-
if (node.value == null) {
153+
} else {
119154
node.value = value;
120155
}
121156
}
@@ -124,6 +159,40 @@ public synchronized void insert(String[] path, int index, T value) {
124159
node.insert(path, index + 1, value);
125160
}
126161

162+
public synchronized void insertOrUpdate(String[] path, int index, T value, BiFunction<T, T, T> updater) {
163+
if (index >= path.length)
164+
return;
165+
166+
String token = path[index];
167+
String key = token;
168+
if (isNamedWildcard(token)) {
169+
key = wildcard;
170+
}
171+
TrieNode node = children.get(key);
172+
if (node == null) {
173+
T nodeValue = index == path.length - 1 ? value : null;
174+
node = new TrieNode(token, nodeValue, wildcard);
175+
addInnerChild(key, node);
176+
} else {
177+
if (isNamedWildcard(token)) {
178+
node.updateKeyWithNamedWildcard(token);
179+
}
180+
/*
181+
* If the target node already exists, but is without a value,
182+
* then the value should be updated.
183+
*/
184+
if (index == (path.length - 1)) {
185+
if (node.value != null) {
186+
node.value = updater.apply(node.value, value);
187+
} else {
188+
node.value = value;
189+
}
190+
}
191+
}
192+
193+
node.insertOrUpdate(path, index + 1, value, updater);
194+
}
195+
127196
private boolean isNamedWildcard(String key) {
128197
return key.indexOf('{') != -1 && key.indexOf('}') != -1;
129198
}
@@ -136,23 +205,57 @@ private boolean isNamedWildcard() {
136205
return namedWildcard != null;
137206
}
138207

139-
public T retrieve(String[] path, int index, Map<String, String> params) {
208+
public T retrieve(String[] path, int index, Map<String, String> params, TrieMatchingMode trieMatchingMode) {
140209
if (index >= path.length)
141210
return null;
142211

143212
String token = path[index];
144213
TrieNode node = children.get(token);
145214
boolean usedWildcard;
215+
146216
if (node == null) {
147-
node = children.get(wildcard);
148-
if (node == null) {
217+
if (trieMatchingMode == TrieMatchingMode.WILDCARD_NODES_ALLOWED) {
218+
node = children.get(wildcard);
219+
if (node == null) {
220+
return null;
221+
}
222+
usedWildcard = true;
223+
} else if (trieMatchingMode == TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED && index == 1) {
224+
/*
225+
* Allow root node wildcard matches.
226+
*/
227+
node = children.get(wildcard);
228+
if (node == null) {
229+
return null;
230+
}
231+
usedWildcard = true;
232+
} else if (trieMatchingMode == TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED && index + 1 == path.length) {
233+
/*
234+
* Allow leaf node wildcard matches.
235+
*/
236+
node = children.get(wildcard);
237+
if (node == null) {
238+
return null;
239+
}
240+
usedWildcard = true;
241+
} else {
149242
return null;
150243
}
151-
usedWildcard = true;
152244
} else {
153-
// If we are at the end of the path, the current node does not have a value but there
154-
// is a child wildcard node, use the child wildcard node
155-
if (index + 1 == path.length && node.value == null && children.get(wildcard) != null) {
245+
if (index + 1 == path.length && node.value == null && children.get(wildcard) != null
246+
&& EXPLICIT_OR_ROOT_WILDCARD.contains(trieMatchingMode) == false) {
247+
/*
248+
* If we are at the end of the path, the current node does not have a value but
249+
* there is a child wildcard node, use the child wildcard node.
250+
*/
251+
node = children.get(wildcard);
252+
usedWildcard = true;
253+
} else if (index == 1 && node.value == null && children.get(wildcard) != null
254+
&& trieMatchingMode == TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED) {
255+
/*
256+
* If we are at the root, and root wildcards are allowed, use the child wildcard
257+
* node.
258+
*/
156259
node = children.get(wildcard);
157260
usedWildcard = true;
158261
} else {
@@ -166,16 +269,16 @@ public T retrieve(String[] path, int index, Map<String, String> params) {
166269
return node.value;
167270
}
168271

169-
T res = node.retrieve(path, index + 1, params);
170-
if (res == null && !usedWildcard) {
272+
T nodeValue = node.retrieve(path, index + 1, params, trieMatchingMode);
273+
if (nodeValue == null && !usedWildcard && trieMatchingMode != TrieMatchingMode.EXPLICIT_NODES_ONLY) {
171274
node = children.get(wildcard);
172275
if (node != null) {
173276
put(params, node, token);
174-
res = node.retrieve(path, index + 1, params);
277+
nodeValue = node.retrieve(path, index + 1, params, trieMatchingMode);
175278
}
176279
}
177280

178-
return res;
281+
return nodeValue;
179282
}
180283

181284
private void put(Map<String, String> params, TrieNode node, String value) {
@@ -200,18 +303,47 @@ public void insert(String path, T value) {
200303
return;
201304
}
202305
int index = 0;
203-
// supports initial delimiter.
306+
// Supports initial delimiter.
204307
if (strings.length > 0 && strings[0].isEmpty()) {
205308
index = 1;
206309
}
207310
root.insert(strings, index, value);
208311
}
209312

313+
/**
314+
* Insert a value for the given path. If the path already exists, replace the value with:
315+
* <pre>
316+
* value = updater.apply(oldValue, newValue);
317+
* </pre>
318+
* allowing the value to be updated if desired.
319+
*/
320+
public void insertOrUpdate(String path, T value, BiFunction<T, T, T> updater) {
321+
String[] strings = path.split(SEPARATOR);
322+
if (strings.length == 0) {
323+
if (rootValue != null) {
324+
rootValue = updater.apply(rootValue, value);
325+
} else {
326+
rootValue = value;
327+
}
328+
return;
329+
}
330+
int index = 0;
331+
// Supports initial delimiter.
332+
if (strings.length > 0 && strings[0].isEmpty()) {
333+
index = 1;
334+
}
335+
root.insertOrUpdate(strings, index, value, updater);
336+
}
337+
210338
public T retrieve(String path) {
211-
return retrieve(path, null);
339+
return retrieve(path, null, TrieMatchingMode.WILDCARD_NODES_ALLOWED);
212340
}
213341

214342
public T retrieve(String path, Map<String, String> params) {
343+
return retrieve(path, params, TrieMatchingMode.WILDCARD_NODES_ALLOWED);
344+
}
345+
346+
public T retrieve(String path, Map<String, String> params, TrieMatchingMode trieMatchingMode) {
215347
if (path.length() == 0) {
216348
return rootValue;
217349
}
@@ -220,10 +352,56 @@ public T retrieve(String path, Map<String, String> params) {
220352
return rootValue;
221353
}
222354
int index = 0;
223-
// supports initial delimiter.
355+
356+
// Supports initial delimiter.
224357
if (strings.length > 0 && strings[0].isEmpty()) {
225358
index = 1;
226359
}
227-
return root.retrieve(strings, index, params);
360+
361+
return root.retrieve(strings, index, params, trieMatchingMode);
362+
}
363+
364+
/**
365+
* Returns an iterator of the objects stored in the {@code PathTrie}, using
366+
* all possible {@code TrieMatchingMode} modes. The {@code paramSupplier}
367+
* is called between each invocation of {@code next()} to supply a new map
368+
* of parameters.
369+
*/
370+
public Iterator<T> retrieveAll(String path, Supplier<Map<String, String>> paramSupplier) {
371+
return new PathTrieIterator<>(this, path, paramSupplier);
372+
}
373+
374+
class PathTrieIterator<T> implements Iterator<T> {
375+
376+
private final List<TrieMatchingMode> modes;
377+
private final Supplier<Map<String, String>> paramSupplier;
378+
private final PathTrie<T> trie;
379+
private final String path;
380+
381+
PathTrieIterator(PathTrie trie, String path, Supplier<Map<String, String>> paramSupplier) {
382+
this.path = path;
383+
this.trie = trie;
384+
this.paramSupplier = paramSupplier;
385+
this.modes = new ArrayList<>(Arrays.asList(TrieMatchingMode.EXPLICIT_NODES_ONLY,
386+
TrieMatchingMode.WILDCARD_ROOT_NODES_ALLOWED,
387+
TrieMatchingMode.WILDCARD_LEAF_NODES_ALLOWED,
388+
TrieMatchingMode.WILDCARD_NODES_ALLOWED));
389+
assert TrieMatchingMode.values().length == 4 : "missing trie matching mode";
390+
}
391+
392+
@Override
393+
public boolean hasNext() {
394+
return modes.isEmpty() == false;
395+
}
396+
397+
@Override
398+
public T next() {
399+
if (modes.isEmpty()) {
400+
throw new NoSuchElementException("called next() without validating hasNext()! no more modes available");
401+
}
402+
TrieMatchingMode mode = modes.remove(0);
403+
Map<String, String> params = paramSupplier.get();
404+
return trie.retrieve(path, params, mode);
405+
}
228406
}
229407
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.rest;
21+
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
import java.util.Set;
26+
27+
/**
28+
* Encapsulate multiple handlers for the same path, allowing different handlers for different HTTP verbs.
29+
*/
30+
final class MethodHandlers {
31+
32+
private final String path;
33+
private final Map<RestRequest.Method, RestHandler> methodHandlers;
34+
35+
MethodHandlers(String path, RestHandler handler, RestRequest.Method... methods) {
36+
this.path = path;
37+
this.methodHandlers = new HashMap<>(methods.length);
38+
for (RestRequest.Method method : methods) {
39+
methodHandlers.put(method, handler);
40+
}
41+
}
42+
43+
/**
44+
* Add an additional method and handler for an existing path. Note that {@code MethodHandlers}
45+
* does not allow replacing the handler for an already existing method.
46+
*/
47+
public MethodHandlers addMethod(RestRequest.Method method, RestHandler handler) {
48+
RestHandler existing = methodHandlers.putIfAbsent(method, handler);
49+
if (existing != null) {
50+
throw new IllegalArgumentException("Cannot replace existing handler for [" + path + "] for method: " + method);
51+
}
52+
return this;
53+
}
54+
55+
/**
56+
* Add a handler for an additional array of methods. Note that {@code MethodHandlers}
57+
* does not allow replacing the handler for an already existing method.
58+
*/
59+
public MethodHandlers addMethods(RestHandler handler, RestRequest.Method... methods) {
60+
for (RestRequest.Method method : methods) {
61+
addMethod(method, handler);
62+
}
63+
return this;
64+
}
65+
66+
/**
67+
* Return an Optional-wrapped handler for a method, or an empty optional if
68+
* there is no handler.
69+
*/
70+
public Optional<RestHandler> getHandler(RestRequest.Method method) {
71+
return Optional.ofNullable(methodHandlers.get(method));
72+
}
73+
74+
/**
75+
* Return a set of all valid HTTP methods for the particular path
76+
*/
77+
public Set<RestRequest.Method> getValidMethods() {
78+
return methodHandlers.keySet();
79+
}
80+
}

0 commit comments

Comments
 (0)