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 }