View Javadoc
1   package com.exsoinn.util.epf;
2   
3   import net.jcip.annotations.Immutable;
4   
5   import java.util.*;
6   import java.util.concurrent.ConcurrentHashMap;
7   import java.util.regex.Matcher;
8   import java.util.regex.Pattern;
9   import java.util.stream.Collectors;
10  
11  
12  /**
13   * Abstract implementation of {@link Context}. This class should be suitable for all cases, however users are free to implement
14   * {@link Context} from scratch, provided the contract is upheld.
15   *
16   * The caller can specify the matching style/behavior on either the filter values provided in the {@link Filter} object, or
17   * the values configured on the target {@link Context} being search. Both cannot be specified though, it's either one or
18   * the other. In case caller has specified both, the code gives preference to {@link Context} matching style/behavior. This
19   * behavior is controlled by either:
20   *   - Passing flag {@link AbstractContext#FOUND_ELEM_VAL_IS_REGEX}, and optionally flag {@link AbstractContext#PARTIAL_REGEX_MATCH}
21   *     in the extra parameters {@code Map} argument that {@link Context#findElement(SearchPath, Filter, TargetElements, Map)}
22   *     accepts. This affects behavior on the {@code Context} only. The former flag says that for filtering purposes, the
23   *     relevant {@code Context} values should behave as if they were regular expressions, in which case the code will
24   *     use a {@link Pattern} to make the comparison on the <strong>entire</strong> filter key value, meaning they both have to match
25   *     exactly. Internally code uses {@link Matcher#matches()} method, read that documentation for that for details. But if you want to do
26   *     partial matching only, <strong>in addition </strong> pass latter flag as well. Internally the code will use method
27   *     {@link Matcher#find()}  to do these kind of partial matches. Refer to that method's documentation for details.
28   *   - To control behavior on the filter values instead, simply use asterisk (*) on the filter values that should match
29   *     partially against the {@code Context} values in question. Currently only asterisk at beginning or end,
30   *     or both of string are supported. Placement of asterisk that deviates from these will throw exception.
31   *   Note that if you mix both methods above, the first one, regular expression matching will take precedence, and passing wildcards (*)
32   *   in the filter values will have no effect (get completely ignored by code).
33   *
34   *
35   * Created by QuijadaJ on 5/4/2017.
36   */
37  @Immutable
38  abstract class AbstractContext implements Context {
39      private static final char WILD_CARD = '*';
40      private static final Map<String, Pattern> patternCache = new ConcurrentHashMap<>();
41      private static final String ANON_ARY_HANDLE = "anonymousArray";
42  
43  
44      @Override
45      public SearchResult findElement(SearchPath pSearchPath,
46                                      Filter pFilter,
47                                      TargetElements pTargetElements,
48                                      Map<String, String> pExtraParams)
49              throws IllegalArgumentException {
50          Map<String, Context> found = findElement(this, pSearchPath, pFilter, pTargetElements, null, pExtraParams);
51          return SearchResult.createSearchResult(found);
52      }
53  
54      @Override
55      public SearchResult findElement(SelectionCriteria pSelectCriteria,
56                                      Map<String, String> pExtraParams) throws IllegalArgumentException {
57          return findElement(
58                  pSelectCriteria.getSearchPath(), pSelectCriteria.getFilter(), pSelectCriteria.getTargetElements(), pExtraParams);
59      }
60  
61  
62      Map<String, Context> findElement(Context pElem,
63                                       SearchPath pSearchPath,
64                                       Filter pFilter,
65                                       TargetElements pTargetElements,
66                                       Map<String, Context> pFoundElemVals,
67                                       Map<String, String> pExtraParams)
68              throws IllegalArgumentException {
69  
70          if (null == pFoundElemVals) {
71              pFoundElemVals = new HashMap<>(pTargetElements != null ? pTargetElements.size() : 0);
72          }
73  
74          String curNodeInPath;
75  
76          /*
77           * Advance the to the next element/node in search path. Because the object is immutable, it's a
78           * 2-step process to do so. Read the API javadoc of SearchPath for more details.
79           */
80          pSearchPath = pSearchPath.advanceToNextNode();
81          curNodeInPath = pSearchPath.currentNode();
82  
83          /**
84           * Deal with case where the original Context given is an anonymous array. In this scenario we expect search path
85           * to be "[N]||nodeX||nodeY||nodeZ||...". The way we handle is that we make the array non-anonymous and identify it by
86           * {@link this#ANON_ARY_HANDLE}, then modify the current node in search path by adding {@link this#ANON_ARY_HANDLE}
87           * in front of the "[]", and finally we let the logic further below deal with an array inside recursible we've just
88           * created. That code already does all checks, throws exception where appropriate, etc.
89           */
90          if (pSearchPath.currentNodeIndex() == 0 && curNodeInPath.indexOf("[") == 0 && pElem.isArray()) {
91              MutableContext mc = ContextFactory.obtainMutableContext("{}");
92              mc.addMember(ANON_ARY_HANDLE, ContextFactory.obtainContext(pElem.stringRepresentation()));
93              curNodeInPath = ANON_ARY_HANDLE + curNodeInPath;
94              pElem = mc;
95          }
96  
97          String curNodeInPathNoBrackets = curNodeInPath;
98          if (arrayIndex(curNodeInPath) >= 0) {
99              curNodeInPathNoBrackets = removeBrackets(curNodeInPath);
100         }
101 
102         boolean atEndOfSearchPath = pSearchPath.isAtEndOfSearchPath();
103 
104 
105         /**
106          * If below if() is true, then we're dealing with a complex structure. At this
107          * point check if the current node in the search path we've been given exists in the current
108          * element. If not, or if it designates an array node with index > 0 yet encountered node
109          * is not in fact of type array, then it means the element will not be found, hence throw
110          * IllegalArgumentException, unless the {@link Context#IGNORE_INCOMPATIBLE_SEARCH_PATH_PROVIDED_ERROR} was
111          * passed in the extra parameters map. The full search path given has to exist in order to return any results.
112          */
113         Set<Map.Entry<String, Context>> elemEntries = null;
114         if (pElem.isRecursible()) {
115             /**
116              * The 'arrayIndex()...' condition is there to see if caller expects array node to be found yet actual
117              * is not an array, and they specified an index greater than 1, in which case throw exception unless
118              * we were specifically instructed to ignore such scenarios (via presence
119              * of {@link Context#IGNORE_INCOMPATIBLE_SEARCH_PATH_PROVIDED_ERROR)}).
120              * We're interested in aforementioned check for non-array nodes only.
121              */
122             if (pElem.containsElement(curNodeInPathNoBrackets) && (arrayIndex(curNodeInPath) <= 0
123                     || pElem.memberValue(curNodeInPathNoBrackets).isArray())) {
124                 /**
125                  * Check inverse of "UnexpectedArrayNodeException" further below; a none-array node encountered,
126                  * yet search path told to expect array here. Unless the array index is 0, throw exception. The
127                  * motivation to make an exception if array index is 0 is to offer some flexibility to calling code. The same
128                  * data node can sometimes be an array, and at others a non-array. This can happen when there's no
129                  * schema backing things up, and in data conversion situations, the target data uses presence
130                  * of multi node or single to display respectively as array or not. A concrete example:
131                  * <xml><node>...</node><node></node></xml> -> {xml: {node: [{}, {}]}}
132                  *
133                  * or
134                  *
135                  * <xml><node>...</node></xml> -> {xml: {node: {}}}
136                  *
137                  * Notice in first, the node is array, in second it's not. It all depends on how original
138                  * data looked. The rationale for this logic is as follows:
139                  * The client just wants the first node in array if index specified is [0], therefore
140                  * give it to them if it is a none-array, which obviously is a single element. However if client
141                  * gave [idx > 0], then I'm confused and don't know what to do, so throw it back to client
142                  * to decide what they want to do.
143                  */
144 
145                 elemEntries = pElem.entrySet();
146             } else {
147 
148                 /**
149                  * Have to wrap into an IllegalArgumentException because the method signature says so. When it was
150                  * decided to throw a checked exception, namely IncompatibleSearchPathException, there would have had
151                  * to be a lot of changes made in dependent code to reflect an updated method signature. Hence the reason
152                  * the below exception wrapping is made.
153                  */
154                 if (null != pExtraParams && pExtraParams.containsKey(IGNORE_INCOMPATIBLE_SEARCH_PATH_PROVIDED_ERROR)) {
155                     /*
156                      * Handles case where caller instructed this API to ignore it if search path is not
157                      * applicable for node in question. In such cases the node simply gets ignored and is excluded from search
158                      * results.
159                      */
160                     return pFoundElemVals;
161                 } else {
162                     IncompatibleSearchPathException ispe = new IncompatibleSearchPathException(
163                             pSearchPath, curNodeInPathNoBrackets, pElem);
164                     throw new IllegalArgumentException(ispe);
165                 }
166 
167             }
168         }
169 
170         /**
171          * If "elemEntries" is not NULL, it means we're dealing with a complex structure (I.e. not a primitive)
172          * and the current element in the search path has been found at this location of the passed in element to search.
173          * Why am I constructing if() statements like this instead of nesting them? Makes code easier to read and
174          * hence maintain, less nesting which means less indentation.
175          */
176         if (null != elemEntries) {
177             for (Map.Entry<String, Context> elemEntry : elemEntries) {
178                 /*
179                  * If this pFoundElemVals is not empty, exit, no need to process further. It means we reached
180                  * the last node in search path and found the goods. This was added here so that JVM does not
181                  * continue iterating if there's more than one element in the element node that contains the element we're
182                  * searching for.
183                  */
184                 if (!pFoundElemVals.isEmpty()) {
185                     return pFoundElemVals;
186                 }
187 
188                 String curElemName = elemEntry.getKey();
189 
190                 if (!curNodeInPathNoBrackets.equals(curElemName)) {
191                     continue;
192                 }
193 
194                 Context elemToProcessNext = elemEntry.getValue();
195                 /*
196                  * If the current element is of type array, deal with it below. If we're *not* at the last node
197                  * of the search path, enforce requirement that user must specify which array entry to select
198                  * to continue on that path of the search.
199                  * Otherwise, if we're already at last node of search path, the requirement is relaxed, and caller has
200                  * option of either specifying and array entry to select, or just select the entire array.
201                  */
202                 if (elemToProcessNext.isArray()) {
203                     /*
204                      * If we're not at end of search path and we encountered an array node, yet the search path
205                      * did not tell us to expect an array at this spot of the search path, throw exception. If the
206                      * caller does not explicitly say what array entry to select, how do we know which path to continue on?
207                      * Also if we didn't enforce this, then it might result in hard to trace bugs in the callers code.
208                      * This is the inverse of check further above, where error is thrown if search path said to expect
209                      * an array but the actual node is not an array.
210                      * Note that this rule is relaxed if the array contains only one entry; in such a case, the client code
211                      * is not required to specify in the search path that the node is an array, the code will
212                      * auto select the only choice, namely the only array entry.
213                      */
214                     if (arrayIndex(curNodeInPath) < 0 && !atEndOfSearchPath && elemToProcessNext.asArray().size() > 1) {
215                         UnexpectedArrayNodeException uane =
216                                 new UnexpectedArrayNodeException(pSearchPath, curNodeInPath, elemToProcessNext);
217                         throw new IllegalArgumentException(uane);
218                     }
219 
220 
221                     /**
222                      * The search path did specify what array entry to grab, deal with that logic in the if() block
223                      * below. Then further below this "if()" we check if this is the last node of search path
224                      * or not. These two pieces of logic combined is what allows the client to specify what array entry to grab
225                      * from last node, or grab the entire last array node.
226                      */
227                     int aryIdx;
228                     if ((aryIdx = arrayIndex(curNodeInPath)) >= 0) {
229                         /**
230                          * Handles scenario where a node in the search path specifies an array entry that does not exist,
231                          * and caller wants to ignore node-not-found error.
232                          */
233                         if (aryIdx >= elemToProcessNext.asArray().size()) {
234                             if (null != pExtraParams && pExtraParams.containsKey(IGNORE_INCOMPATIBLE_SEARCH_PATH_PROVIDED_ERROR)) {
235                                 return pFoundElemVals;
236                             } else {
237                                 IncompatibleSearchPathException ispe = new IncompatibleSearchPathException(
238                                         pSearchPath, curNodeInPath, elemToProcessNext);
239                                 throw new IllegalArgumentException(ispe);
240                             }
241                         }
242 
243                         elemToProcessNext = elemToProcessNext.entryFromArray(aryIdx);
244                     }
245                 }
246 
247 
248                 /*
249                  * If below evaluates to true, we're at the last node of our search path. Invoke helper
250                  * method to add the elements to results for us.
251                  * WARNING: Watch out, do not alter code below; do "atEndOfSearchPath" first. Once we have reached end of search path,
252                  *   recursion does not make sense. If we didn't do this check first, because the element to process next
253                  *   might be recursible, we might recurse even though we're at end of search path!!!
254                  */
255                 if (atEndOfSearchPath) {
256                     processElement(curElemName, elemToProcessNext, pFilter, pTargetElements, pFoundElemVals, pExtraParams);
257                 } else if (elemToProcessNext.isRecursible()) {
258                     findElement(elemToProcessNext, pSearchPath, pFilter, pTargetElements, pFoundElemVals, pExtraParams);
259                 }
260             }
261         }
262 
263         return pFoundElemVals;
264     }
265 
266 
267     /**
268      * Extracts the index specified between square brackets. If passed in string contains no
269      * square brackets, -1 is returned.
270      *
271      * @param pNode
272      * @return - The intenger contained within square brackets, -1 if no brackets found.
273      */
274     private int arrayIndex(String pNode) {
275         if (pNode.indexOf('[') < 0) {
276             return -1;
277         }
278         return Integer.parseInt(pNode.substring(pNode.indexOf('[') + 1, pNode.indexOf(']')));
279     }
280 
281 
282     private void processElement(String pElemName,
283                                 Context pElem,
284                                 Filter pFilter,
285                                 TargetElements pTargetElements,
286                                 Map<String, Context> pFoundElemVals,
287                                 Map<String, String> pExtraParams) throws IllegalArgumentException {
288         Context elemValToStore = null;
289         /*
290          * Handle case when element in last node of search path is primitive or another complex structure
291          */
292         if (pElem.isPrimitive() || pElem.isRecursible()) {
293             /*
294              * Hm, here the shouldExcludeFromResults() check might not be necessary. Why would the caller give
295              * an element as last node in search path, and also give that element name in the pFilter Map?? In
296              * other words, this might be a scenario that never happens, but leaving code here for now in case
297              * there's something I'm missing.
298              */
299             if (shouldExcludeFromResults(pElemName, pElem, pFilter, pExtraParams)) {
300                 return;
301             }
302 
303             elemValToStore = pElem;
304 
305             /*
306              * The pTargetElems parameter applies only when results contain another complex structure.
307              */
308             if (pElem.isRecursible()) {
309                 elemValToStore = filterUnwantedElements(pElem, pTargetElements, pExtraParams);
310             }
311         } else if (pElem.isArray()) {
312             Iterator<Context> itElem = pElem.asArray().iterator();
313             List<Object> elemValList = new ArrayList<>();
314             itElem.forEachRemaining(elem -> {
315 
316                 /*
317                  * Apply filtering if caller provided one. The shouldExcludeFromResults() method assumes
318                  * that the passed in element (the 2nd argument) is either a primitive or a complex object. This
319                  * logic assumes that an array will never contain an array (for example valid JSON does not allow
320                  * arrays inside arrays, otherwise how in the world can you reference an anonymous array in JSON???), so safely
321                  * invoke shouldExcludeFromResults() with this in mind.
322                  */
323                 if (!shouldExcludeFromResults(pElemName, elem, pFilter, pExtraParams)) {
324                     if (elem.isRecursible()) {
325                         /*
326                          * See comment further above regarding pFilter, same applies here
327                          * to pTargetElements
328                          */
329                         elem = filterUnwantedElements(elem, pTargetElements, pExtraParams);
330                     }
331                     elemValList.add(elem.toString());
332                 }
333             });
334 
335             /*
336              * In the SearchResult we can only store a Context. The below is a lame attempt
337              * to try to convert the array-like structure to a Context object, just so that we're able to obey
338              * the contract of SearchResult. As long as the factory can find a suitable API to handle this,
339              * then I guess it should be OK - all the client code cares about is having a Context object with methods
340              * that behave correctly.
341              */
342             if (!elemValList.isEmpty()) {
343                 elemValToStore = ContextFactory.INSTANCE.obtainContext(elemValList);
344             }
345         } else {
346             throw new IllegalArgumentException("One of the elements to search is of type not currently supported."
347                     + "Element name/type is " + pElemName + "/" + pElem.getClass().getName());
348         }
349 
350         if (null != elemValToStore) {
351             /*
352              * TODO: The below is effectively changing a List to a String, and storing it in Map, if the above found an
353              * TODO: array structure. Re-visit.
354              */
355             pFoundElemVals.put(pElemName, elemValToStore);
356             handleSingleComplexObjectFound(pFoundElemVals, pTargetElements);
357         }
358     }
359 
360     /**
361      * This method should be implemented by child classes to handle {@link TargetElements}, to exclude
362      * elements not contained therein.
363      *
364      * @param pElem
365      * @param pTargetElems
366      * @return
367      */
368     Context filterUnwantedElements(Context pElem, TargetElements pTargetElems, Map<String, String> pExtraParams) {
369         if (null == pTargetElems) {
370             return pElem;
371         }
372 
373         MutableContext mc = ContextFactory.INSTANCE.obtainMutableContext("{}");
374         /*
375          * Handle any target element that is one or more levels
376          * deeper than found node.
377          */
378         for (String e : pTargetElems) {
379             if (!stringIsASearchPath(e)) {
380                 continue;
381             }
382             SearchPath sp = SearchPath.valueOf(e);
383             SearchResult sr = pElem.findElement(sp, null, null, pExtraParams);
384 
385             if (null == sr || sr.size() != 1) {
386                 /*
387                  * If caller said to ignore it if the target element search path is not valid for a node, then do so and continue
388                  * processing other target elements provided.
389                  */
390                 if (null != pExtraParams && pExtraParams.containsKey(IGNORE_INCOMPATIBLE_TARGET_ELEMENT_PROVIDED_ERROR) &&
391                         (sr == null || sr.isEmpty())) {
392                     continue;
393                 } else {
394                     throw new IllegalArgumentException("Either found more than one element for target element search path "
395                             + sp.toString() + ", or did not find any results. Check the search path and try again. Results "
396                             + "were " + (null == sr ? "NULL" : "EMPTY") + ". Target elements is " + pTargetElems.toString()
397                             + " and node is " + pElem.stringRepresentation());
398                 }
399             }
400 
401             Map.Entry<String, Context> found = sr.entrySet().iterator().next();
402             mc.addMember(sp.toString(), found.getValue());
403         }
404 
405         /*
406          * Now exclude any other element that was not requested by the caller.
407          */
408         Set<Map.Entry<String, Context>> ents = pElem.entrySet();
409         ents.stream().filter(entry -> pTargetElems.contains(entry.getKey()))
410                 .forEach(entry -> mc.addMember(entry.getKey(), entry.getValue()));
411 
412         return ContextFactory.INSTANCE.obtainContext(mc.stringRepresentation());
413     }
414 
415 
416     /**
417      * This method is applicable only when a {@link TargetElements} has been passed by the calling code, and the
418      * data found in the last node of search path is a single complex object, or a list-like object with a single complex
419      * object as member. In either case it should contain just one name/value pair. It will take the sole name/value pair
420      * of the complex object and store it in passed in {@param pSearchResult} {@link Map}, first clearing any results
421      * contained in {@param pSearchResult}. The idea here is to make it convenient for client code to access the single
422      * name/value pair found without having to do any additional checks, effectively shifting that burden onto this API. The
423      * calling code can just just blindly get the key and value as-is from the search results map.
424      *
425      * @param pSearchRes
426      * @param pTargetElems
427      */
428     void handleSingleComplexObjectFound(Map<String, Context> pSearchRes,
429                                         Set<String> pTargetElems) {
430         if (null == pTargetElems || pTargetElems.isEmpty()) {
431             return;
432         }
433         try {
434             Set<Map.Entry<String, Context>> entries = pSearchRes.entrySet();
435             if (entries.size() != 1) {
436                 return;
437             }
438 
439             final Context elem = entries.iterator().next().getValue();
440             Context ctx;
441             // If element is inside an array, unwrap it first, else grab as is
442             if (elem.isArray() && elem.asArray().size() == 1) {
443                 Context aryElem = elem.asArray().iterator().next();
444                 ctx = aryElem;
445             } else {
446                 ctx = elem;
447             }
448 
449             /*
450              * Now check that element (whether found inside an array or not, see above) is a
451              * complex object and with a single name/value pair, otherwise return w/o doing anything.
452              * Hint: If element is recursible in OR, then entrySet check below will not fail, because of if()
453              *   logic optimization in OR statements done by JVM, where it stops evaluating when truthfulness has been established
454              */
455             if (!ctx.isRecursible() || ctx.entrySet().size() > 1) {
456                 return;
457             }
458 
459             if (null != ctx) {
460                 pSearchRes.clear();
461                 Set<Map.Entry<String, Context>> ctxEntSet = ctx.entrySet();
462 
463                 for (Map.Entry<String, Context> entry : ctxEntSet) {
464                     if (null != pTargetElems && pTargetElems.contains(entry.getKey())) {
465                         /*
466                          * If the key is a search path, convert to single string by removing the leading nodes
467                          * and leaving only the last
468                          */
469                         String k = entry.getKey();
470                         if (stringIsASearchPath(k)) {
471                             k = SearchPath.valueOf(k).lastNode();
472                         }
473                         pSearchRes.put(k, entry.getValue());
474                     }
475                 }
476             }
477         } catch (Exception e) {
478             System.err.println("There was a problem: " + e);
479         }
480     }
481 
482     private boolean stringIsASearchPath(String pStr) {
483         return pStr.indexOf(".") > 0;
484     }
485 
486 
487     /**
488      * Contains logic to handle {@link Filter} <code>pFilter</code> param. The {@link Filter} is nothing more than
489      * a list of name/value pairs used to further refine search result. Note that the field names in the
490      * {@link Filter} can also themselves be search paths, specified in string format using dot separated token
491      * notation (see {@link SearchPath#valueOf(String)} for more details), for example:
492      * <p>
493      * field1.field2[0].field3
494      * <p>
495      * This is meant to filter nodes on fields which are one or more levels deeper from the found node itself.
496      * <p>
497      * See {@link Context#findElement(SearchPath, Filter, TargetElements, Map)} for more details on how the
498      * {@link Filter} argument is handled.
499      * <p>
500      * This method assumes that the passed in {@param pElem} is either a primitive or a complex object, and will
501      * <strong>never</strong> be an array, else {@link IllegalArgumentException} will get thrown!!! The calling code
502      * is expected to do the appropriate checks.
503      *
504      * @param pElemName - Matters only in the context of a search result which is a primitive
505      * @param pElem     - The {@code Context} object to which the {@param pFilter} gets applied.
506      * @param pFilter   - The {@code Filter} object to use to refine search results.
507      * @return - <code>true</code> if the data should be excluded from the search results, <code>false</code>
508      * otherwise
509      */
510     boolean shouldExcludeFromResults(String pElemName, Context pElem, Filter pFilter, Map<String, String> pExtraParams)
511             throws IllegalArgumentException {
512         if (null == pFilter) {
513             return false;
514         }
515 
516         if (pElem.isArray()) {
517             throw new IllegalArgumentException("Got an array element when applying search filter "
518                     + pFilter.entrySet().stream().map(Map.Entry::toString).collect(Collectors.joining())
519                     + ". The element in question is " + pElemName + " ===>>> " + pElem.stringRepresentation());
520         }
521 
522         /*
523          * Check up front if filter is not applicable to found results,
524          * throw runtime exception if that's the case.
525          */
526         StringBuilder filterNotApplicableReason = new StringBuilder();
527         if (!filterIsApplicableToFoundElement(pElem, pElemName, pFilter, filterNotApplicableReason)) {
528             throw new IllegalArgumentException("Filter not applicable to found element: " + filterNotApplicableReason.toString());
529         }
530 
531         Set<Map.Entry<String, String>> filterEntries = pFilter.entrySet();
532         for (Map.Entry<String, String> filterEntry : filterEntries) {
533             String filterKey = filterEntry.getKey();
534             String filterVal = filterEntry.getValue();
535 
536             if (stringIsASearchPath(filterKey)) {
537                 // TODO: Throw exception when the found element is a primitive? Reasoning is that nested filter element applies only
538                 // TODO: when search result is a non-primitive
539                 /**
540                  * Handles case when the value we want to filter on is buried one or more levels deeper than the
541                  * found element.
542                  * We leverage findElement(), which accepts a dot (.) separated element search path. Also, we support only filtering
543                  * on primitive values, therefore assume that the found element will be a single name value pair.
544                  * If the path of the filter element is not found, IllegalArgumentException is thrown.
545                  */
546                 SearchPath elemSearchPath = SearchPath.valueOf(filterKey);
547                 // Comparison has to be done with brackets removed from filtering key, else comparison is not valid
548                 // and this will return true, because the Context member name does not have brackets
549                 if (!pElem.containsElement(removeBrackets(elemSearchPath.get(0)))) {
550                     /*
551                      * Return true because the top node of the specified search path
552                      * was not even found in this context
553                      */
554                     return true;
555                 }
556 
557 
558                 Map<String, Context> nestedElemSearchRes;
559                 nestedElemSearchRes = findElement(pElem, elemSearchPath, null, null, null, pExtraParams);
560                 Context nestedElemCtx;
561                 if (null != nestedElemSearchRes && nestedElemSearchRes.size() > 0) {
562                     Set<Map.Entry<String, Context>> entries = nestedElemSearchRes.entrySet();
563                     nestedElemCtx = entries.iterator().next().getValue();
564                 } else {
565                     /*
566                      * When the nested filter key (I.e. search path) failed to find results, simply ignore it if caller
567                      * so has instructed, else throw exception.
568                      */
569                     if (null == pExtraParams || !pExtraParams.containsKey(IGNORE_INCOMPATIBLE_SEARCH_PATH_PROVIDED_ERROR)) {
570                         throw new IllegalArgumentException("The filter element value specified was not found off of this node: " +
571                                 filterKey);
572                     } else {
573                         return true;
574                     }
575                 }
576 
577                 if (!filterValueMatches(nestedElemCtx, filterVal, pExtraParams)) {
578                     return true;
579                 }
580             } else {
581                 Context elem;
582                 if (pElem.isPrimitive()) {
583                     elem = pElem;
584                 } else {
585                     elem = pElem.memberValue(filterKey);
586                 }
587 
588 
589                 if (!filterValueMatches(elem, filterVal, pExtraParams)) {
590                     return true;
591                 }
592             }
593         }
594         return false;
595     }
596 
597 
598     /**
599      * Processes a value from the {@link Filter} provided by caller against the found region that it applies
600      * to of the {@link Context} passed to {@link Context#findElement(SelectionCriteria, Map)} (or
601      * {@link Context#findElement(SearchPath, Filter, TargetElements, Map)} method.
602      * context that was provided in
603      *
604      * @param pFoundElem
605      * @param pFilterVal
606      * @param pExtraParams
607      * @return
608      * @throws IllegalArgumentException
609      */
610     boolean filterValueMatches(Context pFoundElem, String pFilterVal, Map<String, String> pExtraParams)
611             throws IllegalArgumentException {
612 
613         if (pFoundElem.isArray()) {
614             /**
615              * Handle scenario where the target data to apply filter to is an array of values. If
616              * that's the case, then each array entry is compared to the supplied filter value. If match
617              * is found in any of the array elements, then the element should *not* be excluded from
618              * the search results.
619              */
620             return pFoundElem.asArray()
621                     .parallelStream().anyMatch(v -> filterValueAndFoundValueMatch(v.stringRepresentation(),
622                             pFilterVal, pExtraParams));
623 
624         } else {
625             return filterValueAndFoundValueMatch(pFoundElem.stringRepresentation(), pFilterVal, pExtraParams);
626 
627         }
628     }
629 
630 
631     /**
632      * Implements logic to check if a value found in the search {@link Context} object matches a given value
633      * from the {@link Filter} object provided by the caller.
634      *
635      * @param pFoundVal
636      * @param pFilterVal
637      * @param pExtraParams
638      * @return
639      * @throws IllegalArgumentException
640      */
641     boolean filterValueAndFoundValueMatch(String pFoundVal, String pFilterVal, Map<String, String> pExtraParams)
642             throws IllegalArgumentException {
643         /**
644          * See if caller has requested that the values in the {@code Context} themselves behave
645          * as regular expressions for purposes of filtering. In this case we ignore the matching style requested
646          * for the filter key values, and instead just do the RegEx logic below
647          */
648         if (null != pExtraParams && pExtraParams.containsKey(FOUND_ELEM_VAL_IS_REGEX)) {
649             /*
650              * For performance gains, cache already seen regex patterns, and retrieve from
651              * cache if same regex comes again.
652              */
653             Pattern p = patternCache.get(pFoundVal);
654             if (null == p) {
655                 p = Pattern.compile(pFoundVal);
656                 Pattern prevPatt = patternCache.putIfAbsent(pFoundVal, p);
657                 if (null != prevPatt) {
658                     p = prevPatt;
659                 }
660             }
661             List<String> filterVals = buildListFilter(pFilterVal);
662 
663             for (String f : filterVals) {
664                 Matcher m = p.matcher(f);
665                 if (pExtraParams.containsKey(PARTIAL_REGEX_MATCH)) {
666                     if (m.find()) {
667                         return true;
668                     }
669                 } else {
670                     if (m.matches()) {
671                         return true;
672                     }
673                 }
674             }
675 
676             return false;
677         }
678 
679         /**
680          * See if the filter value is an array of values. Below method call will create list of those values. If it's
681          * a single value, the resulting list will contain just that one entry.
682          * Then loop over each filter value, and return at the first match of filter value against found value. Else
683          * return false because none of the filter values matched.
684          */
685         List<String> filterVals = buildListFilter(pFilterVal);
686         for (String filterVal : filterVals) {
687             boolean unsupportedWildCardPlacement = true;
688             // If no wild card found, do exact match
689             if (filterVal.indexOf(WILD_CARD) < 0) {
690                 unsupportedWildCardPlacement = false;
691                 if (pFoundVal.equals(filterVal)) {
692                     return true;
693                 }
694             }
695 
696             String filterValWithoutWildCard = filterVal.replaceAll("\\*", "");
697             // Wild card found at both beginning and end, to a partial match comparison
698             if (filterVal.charAt(0) == WILD_CARD && filterVal.charAt(filterVal.length() - 1) == WILD_CARD) {
699                 unsupportedWildCardPlacement = false;
700                 if (pFoundVal.indexOf(filterValWithoutWildCard) >= 0) {
701                     return true;
702                 }
703             } else if (filterVal.charAt(0) == WILD_CARD) {
704                 unsupportedWildCardPlacement = false;
705                 // Must match at end
706                 if (pFoundVal.lastIndexOf(filterValWithoutWildCard) == pFoundVal.length() - filterValWithoutWildCard.length()) {
707                     return true;
708                 }
709 
710             } else if (filterVal.charAt(filterVal.length() - 1) == WILD_CARD) {
711                 unsupportedWildCardPlacement = false;
712                 // Must match at beginning
713                 if (pFoundVal.indexOf(filterValWithoutWildCard) == 0) {
714                     return true;
715                 }
716             }
717 
718             if (unsupportedWildCardPlacement) {
719                 throw new IllegalArgumentException("Illegal placement of wildcard character '" + WILD_CARD
720                         + "' found in filter value '" + filterVal + "'. Only begin/end, or either begin or end wildcard"
721                         + " placement is supported.");
722             }
723         }
724 
725         return false;
726     }
727 
728 
729     private List<String> buildListFilter(String pFilterVal) {
730         List<String> filterVals;
731         if (null == (filterVals = Context.transformArgumentToListObject(pFilterVal))) {
732             filterVals = new ArrayList<>();
733             filterVals.add(pFilterVal);
734         }
735 
736         return filterVals;
737     }
738 
739 
740     /**
741      * Checks if the caller passed in a {@code Filter} that applies to expected search results as per
742      * {@code SearchPath} provided. The exception to the rule is for when a filter key is a for an element
743      * one or more levels deeper than found search results (in the case of found search results being a
744      * complex object). In such cases the requirement is relaxed, because of the case when not all nodes found
745      * contain the nested element filter key passed.
746      * <p>
747      * TODO: Might need to relax as well for case when search results is two or more complex objects, and not all of them
748      * TODO: contain the same set of keys, and one of those keys is being used as filter. Under current logic, such
749      * TODO: scenario would throw error.
750      *
751      * @param pFoundCtx
752      * @param pFoundElemName
753      * @param pFilter
754      * @param pReason
755      * @return
756      */
757     boolean filterIsApplicableToFoundElement(Context pFoundCtx, String pFoundElemName, Filter pFilter, StringBuilder pReason) {
758         if (pFoundCtx.isPrimitive() && pFilter.size() > 1) {
759             if (null != pReason) {
760                 pReason.append("Found element " + pFoundElemName + " ===>>> " + pFoundCtx.stringRepresentation()
761                         + " is a primitive yet filter contained more than one entry: "
762                         + pFilter.entrySet().stream().map(Map.Entry::toString).collect(Collectors.joining()));
763             }
764             return false;
765         }
766         for (Map.Entry<String, String> e : pFilter.entrySet()) {
767             String k = e.getKey();
768             boolean applies = true;
769 
770             if (pFoundCtx.isPrimitive()) {
771                 if (!pFoundElemName.equals(k)) {
772                     if (null != pReason) {
773                         pReason.append("The supplied filter key does not match for found primitive element name: " + pFoundElemName
774                                 + " ===>>> " + pFoundCtx.stringRepresentation() + ". Filter is "
775                                 + pFilter.entrySet().stream().map(Map.Entry::toString).collect(Collectors.joining()));
776                     }
777                     applies = false;
778                 }
779             } else {
780                 if (stringIsASearchPath(k)) {
781                     continue;
782                 }
783                 if (!pFoundCtx.containsElement(k)) {
784                     if (null != pReason) {
785                         pReason.append("The supplied filter key is not found in complex object: " + pFoundElemName
786                                 + " ===>>> " + pFoundCtx.stringRepresentation() + ". Filter is key " + k
787                                 + ". Full filter provided is "
788                                 + pFilter.entrySet().stream().map(Map.Entry::toString).collect(Collectors.joining(";")));
789                     }
790                     applies = false;
791                 }
792             }
793 
794             if (!applies) {
795                 return false;
796             }
797         }
798 
799         return true;
800     }
801 
802 
803     @Override
804     public SearchPath startSearchPath() {
805         if (isRecursible() && entrySet().size() == 1) {
806             return SearchPath.valueOf(entrySet().iterator().next().getKey());
807         }
808 
809         return null;
810     }
811 
812     public List<String> topLevelElementNames() {
813         if (isRecursible()) {
814             return entrySet().parallelStream().map(Map.Entry::getKey).collect(Collectors.toList());
815         }
816 
817         return null;
818     }
819 
820 
821     private String removeBrackets(String pIn) {
822         if (arrayIndex(pIn) < 0) {
823             return pIn;
824         }
825         return pIn.substring(0, pIn.indexOf('['));
826     }
827 }