1919
2020package org .elasticsearch .common .path ;
2121
22+ import java .util .ArrayList ;
23+ import java .util .Arrays ;
24+ import java .util .EnumSet ;
2225import java .util .HashMap ;
26+ import java .util .HashSet ;
27+ import java .util .Iterator ;
28+ import java .util .List ;
2329import 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
2535import static java .util .Collections .emptyMap ;
2636import static java .util .Collections .unmodifiableMap ;
2737
2838public 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}
0 commit comments