001package Torello.Java;
002
003import Torello.HTML.NodeSearch.TCCompareStrException;
004import Torello.HTML.NodeSearch.TextComparitor;
005
006import java.util.*;
007import java.util.regex.*;
008import java.io.IOException;
009
010import java.util.function.Predicate;
011import java.util.stream.Stream;
012import java.io.Serializable;
013
014/**
015 * A simple functional-interface that provides many 'builder' methods for creating standard
016 * Java <CODE>Predicate's</CODE> that operate on an Object's <CODE>'toString'</CODE> method.
017 * 
018 * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER>
019 */
020@FunctionalInterface
021public interface StrFilter extends Serializable, Predicate<Object>
022{
023    /** <EMBED CLASS='external-html' DATA-FILE-ID=SVUIDFI>  */
024    public static final long serialVersionUID = 1;
025
026
027    // ********************************************************************************************
028    // ********************************************************************************************
029    // Functional-Interface Method
030    // ********************************************************************************************
031    // ********************************************************************************************
032
033
034    /** 
035     * <EMBED CLASS='external-html' DATA-FILE-ID=FUNC_INTER_METH>
036     *
037     * <BR /><BR />This method will receive a {@code java.lang.Object}.  This {@code Object} must
038     * have implemented its {@code toString()} method  The purpose of this method is to provide an
039     * easy means to filter certain {@code String's} (automatically).
040     *
041     * <BR /><BR /><B CLASS=JDDescLabel>Standard Filter Behavior:</B>
042     * 
043     * <BR />This method should return {@code FALSE} if the passed {@code String} <I><B>should be
044     * skipped</B></I>.  A return value of {@code TRUE} implies that the {@code String} is not to
045     * be ignored or passed over, but rather 'kept.'
046     *
047     * <BR /><BR />This behavior is consisten with the Java Stream's method
048     * {@code Stream.filter(Predicate)}.
049     *
050     * @param o This is the {@code Object} that will be tested.  If it passes the test, then this
051     * method must return {@code TRUE} which would imply that the object shall not be filtered.
052     * If the object fails the test, this method should return {@code FALSE}, and the object shall
053     * not be included in the result set as it has failed the test.
054     *
055     * @return When implementing this method, returning {@code TRUE} must mean that the
056     * {@code Object} has passed the filter's test-requirements (and will subsequently be retained
057     * by whatever function is carrying out the filter operation).
058     */
059    public boolean test(Object o);
060
061    /**
062     * Java is not perfect.  This method is fine: since {@code String} inherits {@code Object},
063     * and the {@code interface Predicate} does not have any write-dependencies.
064     * 
065     * <BR /><BR />Erasure is like this with {@code interface Predicate} - because that's how
066     * it works with all of the generics.  A {@code Vector<Object>} cannot be passed to a
067     * {@code Vector<String>} parameter (because a person might need to use the {@code String's}).
068     * But a {@code Predicate<Object>} <B><I>can not</I></B> have problems being passed to a 
069     * {@code Predicate<String>}...
070     *
071     * @return {@code 'this' StrFilter} cast into a {@code Predicate<String>}
072     */
073    @SuppressWarnings({"rawtypes", "unchecked"})
074    public default Predicate<String> castToStrPred()
075    {
076        Predicate p = this;
077        return (Predicate<String>) p;
078    }
079
080
081    // ********************************************************************************************
082    // ********************************************************************************************
083    // strList: KEEP
084    // ********************************************************************************************
085    // ********************************************************************************************
086
087
088    /**
089     * Convenience Method.
090     * <BR />Invokes: {@link #strListKEEP(Iterator, boolean)}
091     */
092    public static StrFilter strListKEEP(Iterable<String> strList, boolean ignoreCase)
093    { return strListKEEP(strList.iterator(), ignoreCase); }
094
095    /**
096     * Convenience Method.
097     * <BR />Invokes: {@link #strListKEEP(Iterator, boolean)}
098     * <BR />Converts: {@code String[]} VarArgs to {@code Iterator<String>}
099     */
100    public static StrFilter strListKEEP(boolean ignoreCase, String... strList)
101    { return strListKEEP(Arrays.asList(strList).iterator(), ignoreCase); }
102
103    /**
104     * Convenience Method.
105     * <BR />Invokes: {@link #strListKEEP(Iterator, boolean)}
106     * <BR />Loads: {@code 'strListFileName'} from disk to an {@code Iterator<String>}
107     * <BR />Ignores: Any blank-lines or lines that begin with the {@code '#'} character.
108     */
109    public static StrFilter strListKEEP(String strListFileName, boolean ignoreCase)
110        throws IOException
111    {
112        return strListKEEP(
113            FileRW
114                .loadFileToStream(strListFileName, false)
115                .filter((String line) -> ! StringParse.onlyWhiteSpace_OrZeroLen.test(line))
116                .filter((String line) -> line.charAt(0) != '#')
117                .iterator(),
118                ignoreCase
119        );
120    }
121
122    /**
123     * Convenience Method.
124     * <BR />Invokes: {@link #strListKEEP(Iterator, boolean)}
125     * <BR />Loads: {@code 'strListFileName'} from disk to an {@code Iterator<String>}
126     * <BR />Ignores: Any blank-lines or lines that begin with the {@code '#'} character.
127     * <BR />Catches Exception, and Fails Fast - (See {@link LFEC})
128     */
129    public static StrFilter strListKEEP_NOIOE(String strListFileName, boolean ignoreCase)
130    {
131        return strListKEEP(
132            LFEC
133                .loadFileToStream(strListFileName, false)
134                .filter((String line) -> ! StringParse.onlyWhiteSpace_OrZeroLen.test(line))
135                .filter((String line) -> line.charAt(0) != '#')
136                .iterator(),
137                ignoreCase
138        );
139    }
140
141    /**
142     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE2>
143     *
144     * <BR /><BR />Here, an input-object's {@code toString()} result is compared against the
145     * {@code String's} contained in {@code 'strList'} - which would also be called an
146     * input-{@code String} 'white-list.'
147     *
148     * @param strList A list of {@code String's} to be used as a 'comparison set' against an
149     * input-{@code String} in a {@code Predicate}-test.
150     *
151     * @param ignoreCase When this is {@code TRUE} all equality-comparisons performed in the
152     * {@code String's} tested will ignore case-sensitivity.
153     *
154     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
155     * according to each of the {@code String's} in the input-parameter {@code 'strList'}
156     */
157    public static StrFilter strListKEEP(Iterator<String> strList, boolean ignoreCase)
158    {
159        // Build a TreeSet<String>, this will be used, internally, in the instance of 'StrFilter'
160        // (a Predicate<Object>) that is returned by this factory-builder method.
161        // 
162        // Experienced Java users: TreeSet is a very convenient and easy-to-use "sorted list"
163        // implementation that stores strings, in sorted order, in a binary-tree.  This makes
164        // looking them up as efficient as possible.
165
166        final TreeSet<String> ts = new TreeSet<>();
167
168        // When building this sorted-list (TreeSet), if we are ignoring case (and-only-if), then
169        // we must convert the strings contained by the input parameter string-list 'strList' to
170        // lower-case first.
171
172        if (ignoreCase) while (strList.hasNext()) ts.add(strList.next().toLowerCase());
173        else            while (strList.hasNext()) ts.add(strList.next());
174
175        // The returned Predicate<Object> must call '.toString()' on it's object-input before
176        // checking the "Sorted-List" (TreeSet<String>) to see if it contains the string.  If a
177        // case-insensitive predicate was requested, then we must build a Predicate that invokes
178        // BOTH 'toString()' AND THEN invokes 'toLowerCase()' on the input Object.
179
180        if (ignoreCase) return (Object o) -> ts.contains(o.toString().toLowerCase());
181        else            return (Object o) -> ts.contains(o.toString());
182    }
183
184
185    // ********************************************************************************************
186    // ********************************************************************************************
187    // strList REJECT
188    // ********************************************************************************************
189    // ********************************************************************************************
190
191
192    /**
193     * Convenience Method.
194     * <BR />Invokes: {@link #strListREJECT(Iterator, boolean)}
195     */
196    public static StrFilter strListREJECT(Iterable<String> strList, boolean ignoreCase)
197    { return strListREJECT(strList.iterator(), ignoreCase); }
198
199    /**
200     * Convenience Method.
201     * <BR />Invokes: {@link #strListREJECT(Iterator, boolean)}
202     * <BR />Converts: {@code String[]} VarArgs to {@code Iterator<String>}
203     */
204    public static StrFilter strListREJECT(boolean ignoreCase, String... strList)
205    { return strListREJECT(Arrays.asList(strList).iterator(), ignoreCase); }
206
207    /**
208     * Convenience Method.
209     * <BR />Invokes: {@link #strListREJECT(Iterator, boolean)}
210     * <BR />Loads: {@code 'strListFileName'} from disk to an {@code Iterator<String>}
211     */
212    public static StrFilter strListREJECT(String strListFileName, boolean ignoreCase) throws IOException
213    { return strListREJECT(FileRW.loadFileToVector(strListFileName, false).iterator(), ignoreCase); }
214
215    /**
216     * Convenience Method.
217     * <BR />Invokes: {@link #strListREJECT(Iterator, boolean)}
218     * <BR />Loads: {@code 'strListFileName'} from disk to an {@code Iterator<String>}
219     * <BR />Catches Exception, and Fails Fast - (See {@link LFEC})
220     */
221    public static StrFilter strListREJECT_NOIOE(String strListFileName, boolean ignoreCase)
222    { return strListREJECT(LFEC.loadFileToVector(strListFileName, false).iterator(), ignoreCase); }
223
224    /**
225     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE2>
226     *
227     * <BR /><BR />Here, an input-object's {@code toString()} result is compared against the
228     * {@code String's} contained in {@code 'strList'} - which would also be called an
229     * input-{@code String} 'black-list.'
230     *
231     * @param strList A list of {@code String's} to be used as a 'comparison set' against an
232     * input-{@code String} in a {@code Predicate}-test.
233     *
234     * @param ignoreCase When this is {@code TRUE} all equality-comparisons performed in the
235     * {@code String's} tested will ignore case-sensitivity.
236     *
237     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
238     * according to each of the {@code String's} in the input-parameter {@code 'strList'}.
239     */
240    public static StrFilter strListREJECT(Iterator<String> strList, boolean ignoreCase)
241    {
242        // Build a TreeSet<String>, this will be used, internally, in the instance of 'StrFilter'
243        // (a Predicate<Object>) that is returned by this factory-builder method.
244        // 
245        // Experienced Java users: TreeSet is a very convenient and easy-to-use "sorted list"
246        // implementation that stores strings, in sorted order, in a binary-tree.  This makes
247        // looking them up as efficient as possible.
248
249        final TreeSet<String> ts = new TreeSet<>();
250
251        // When building this sorted-list (TreeSet), if we are ignoring case (and-only-if), then
252        // we must convert the strings contained by the input parameter string-list 'strList' to
253        // lower-case first.
254
255        if (ignoreCase) while (strList.hasNext()) ts.add(strList.next().toLowerCase());
256        else            while (strList.hasNext()) ts.add(strList.next());
257
258        // The returned Predicate<Object> must call '.toString()' on it's object-input before
259        // checking the "Sorted-List" (TreeSet<String>) to see if it contains the string.  If a
260        // case-insensitive predicate was requested, then we must build a Predicate that invokes
261        // BOTH 'toString()' AND THEN invokes 'toLowerCase()' on the input Object.
262
263        if (ignoreCase) return (Object o) -> ! ts.contains(o.toString().toLowerCase());
264        else            return (Object o) -> ! ts.contains(o.toString());
265    }
266
267
268    // ********************************************************************************************
269    // ********************************************************************************************
270    // strList Regular-Expressions: Filter Factory / Filter-Generator  static-methods
271    // ********************************************************************************************
272    // ********************************************************************************************
273
274
275    /**
276     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE2>
277     *
278     * <BR /><BR />Here, an input-object's {@code toString()} result is checked against Java's
279     * Regular-Expression matcher.  The {@code Predicate} generated will choose to <B>KEEP</B>
280     * input-{@code Object's} by returning {@code TRUE} when the Regular-Expression matches the
281     * {@code Object.toString()} results.
282     *
283     * @param regEx A regular-expression used to compare against an input-{@code Object} (using
284     * {@code Object.toString()} in a {@code Predicate}-test.
285     *
286     * @param regExMustMatchEntireString When this parameter is passed {@code TRUE}, then the
287     * regular-expression {@code Pattern} must match the <B>entire input {@code Object.toString()}
288     * </B>.   When this parameter is passed {@code FALSE}, it only need match some portion or
289     * part of the input-parameter {@code String} in the {@code Predicate-test} that is being
290     * generated.
291     *
292     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
293     * according to the input-parameter {@code 'regEx'}
294     */
295    public static StrFilter regExKEEP(Pattern regEx, boolean regExMustMatchEntireString)
296    {
297        // FAIL-FAST: Perform this test BEFORE building the regex, to save the programmer
298        // from later on (when using this factory generated predicate) having a
299        // NullPointerException that is more difficult to isolate.
300
301        if (regEx == null) throw new NullPointerException(
302            "The Regular-Expression provided to the 'regEx' parameter of this " +
303            "static-factory-builder method, 'regExKEEP,' was null."
304        );
305
306        if (regExMustMatchEntireString)
307        {
308            // the java.util.regex.Pattern.asPredicate() method produces a predicate that must
309            // match an entire input string.  This would be identical (if it were possible) to
310            // insert a '^' at the beginning of the regex, and an '$' at the end.
311    
312            final Predicate<String> p = regEx.asPredicate();
313
314            return (Object o) -> p.test(o.toString());
315        }
316
317        else
318            return (Object o) -> regEx.matcher(o.toString()).find();
319    }
320
321    /**
322     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE2>
323     *
324     * <BR /><BR />Here, an input-object's {@code toString()} result is checked against Java's
325     * Regular-Expression matcher.  The {@code Predicate} generated will choose to <B>REJECT</B>
326     * input-{@code Object's} by returning {@code FALSE} when the Regular-Expression matches the
327     * {@code Object.toString()} results.
328     *
329     * @param regEx A regular-expression used to compare against an input-{@code Object} (using
330     * {@code Object.toString()} in a {@code Predicate}-test.
331     *
332     * @param regExMustMatchEntireString When this parameter is passed {@code TRUE}, then the
333     * regular-expression {@code Pattern} must match the <B>entire input {@code Object.toString()}
334     * </B>.   When this parameter is passed {@code FALSE}, it only need match some portion or
335     * part of the input-parameter {@code String} in the {@code Predicate-test} that is being
336     * generated.
337     *
338     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
339     * according to the input-parameter {@code 'regEx'}
340     */
341    public static StrFilter regExREJECT(Pattern regEx, boolean regExMustMatchEntireString)
342    {
343        // FAIL-FAST: Perform this test BEFORE building the regex, to save the programmer
344        // from later on (when using this factory generated predicate) having a
345        // NullPointerException that is more difficult to isolate.
346
347        if (regEx == null) throw new NullPointerException(
348            "The Regular-Expression provided to the 'regEx' parameter of this " +
349            "static-factory-builder method, 'regExREJECT,' was null."
350        );
351
352        if (regExMustMatchEntireString)
353        {
354            // the java.util.regex.Pattern.asPredicate() method produces a predicate that must
355            // match an entire input string.  This would be identical (if it were possible) to
356            // insert a '^' at the beginning of the regex, and an '$' at the end.
357
358            final Predicate<String> p = regEx.asPredicate().negate();
359            return (Object o) -> ! p.test(o.toString());
360        }
361        else
362            return (Object o) -> ! regEx.matcher(o.toString()).find();
363    }
364
365
366    // ********************************************************************************************
367    // ********************************************************************************************
368    // strList Regular-Expressions: Filter Factory / Filter-Generator  static-methods
369    // ********************************************************************************************
370    // ********************************************************************************************
371
372
373    /**
374     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE>
375     *
376     * The {@code Predicate} that is created by this factory-method will have a {@code test()}
377     * method that returns {@code TRUE} if an input object's {@code Object.toString()} output
378     * matches <I>each-and-every-one</I> of the provided regular-expressions.
379     *
380     * @param regExs <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_REGEXS>
381     *
382     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
383     * according to the input-parameter {@code 'regExs'}
384     */
385    public static StrFilter regExsAND(Pattern... regExs)
386    {
387        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once
388        // the lambda-predicate is invoked.
389
390        for (Pattern regEx : regExs)
391
392            if (regEx == null) throw new NullPointerException(
393                "One or more of the elements passed to static-factory method 'regexsAND' are " +
394                "null."
395            );
396
397        // This over-comes a minor "possible complication" - without likely causing much
398        // inefficiency.  If a RegEx[] were passed (array, not a '...' list) and that array were
399        // changed after building this Predicate, the Predicate's behavior would fail.
400        // Avoid this (unlikely, but possible) problem by copying the array, and returning a 
401        // predicate that stores a different  array pointer (because it is to a different array)
402        // inside the Predicate's body.
403
404        final Pattern[] pArr = regExs.clone();
405
406        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once
407        // the lambda-predicate is invoked.
408
409        if (pArr.length == 0) throw new IllegalArgumentException(
410            "This static-factory method 'regExsAND' has been invoked with zero-arguments to the " +
411            "'regExs' var-args parameter."
412        );
413
414        return (Object o) ->
415        {
416            String s=o.toString();
417
418            // Cycle the Regular-Expressions.  "AND" means if even one fails, return FALSE
419            // immediately.
420
421            for (Pattern p : pArr) if (! p.matcher(s).find()) return false;
422
423            // None of them failed, return TRUE.
424            return true;
425        };
426    }
427
428    /**
429     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE>
430     *
431     * The {@code Predicate} that is created by this factory-method will have a {@code test()}
432     * method that returns {@code TRUE} if an input object's {@code Object.toString()} output
433     * matches <I>any-one-of</I> the provided regular-expressions.
434     * 
435     * @param regExs <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_REGEXS>
436     *
437     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
438     * according to the input-parameter {@code 'regExs'}
439     */
440    public static StrFilter regExsOR(Pattern... regExs)
441    {
442        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once the
443        // lambda-predicate is invoked.
444
445        for (Pattern regEx : regExs)
446
447            if (regEx == null) throw new NullPointerException(
448                "One or more of the elements passed to static-factory method 'regExsOR' are " +
449                "null."
450            );
451
452        // This over-comes a minor "possible complication" - without likely causing much
453        // inefficiency.  If a RegEx[] were passed (array, not a '...' list) and that array were
454        // changed after building this Predicate, the Predicate's behavior would fail.
455        // Avoid this (unlikely, but possible) problem by copying the array, and returning a 
456        // predicate that stores a different  array pointer (because it is to a different array)
457        // inside the Predicate's body.
458
459        final Pattern[] pArr = regExs.clone();
460
461        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once
462        // the lambda-predicate is invoked.
463
464        if (pArr.length == 0) throw new IllegalArgumentException(
465            "This static-factory method 'regExsOR' has been invoked with zero-arguments to the" +
466            "'regExs' var-args parameter."
467        );
468
469        return (Object o) ->
470        {
471            String s = o.toString();
472
473            // Cycle the Regular-Expressions.  "OR" means if even one succeeds, return TRUE
474            // immediately.
475
476            for (Pattern p : pArr) if (p.matcher(s).find()) return true;
477
478            // None succeeded, so return FALSE.
479            return false;
480        };
481    }
482
483    /**
484     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE>
485     *
486     * The {@code Predicate} that is created by this factory-method will have a {@code test()}
487     * method that returns {@code TRUE} if an input object's {@code Object.toString()} output
488     * <I>does-not-match-any</I> of the provided regular-expressions.
489     *
490     * @param regExs <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_REGEXS>
491     *
492     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
493     * according to the input-parameter {@code 'regExs'}.
494     */
495    public static StrFilter regExsNAND(Pattern... regExs)
496    {
497        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once the
498        // lambda-predicate is invoked.
499
500        for (Pattern regEx : regExs)
501
502            if (regEx == null) throw new NullPointerException(
503                "One or more of the elements passed to static-factory method 'regExsNAND' are " +
504                "null."
505            );
506
507        // This over-comes a minor "possible complication" - without likely causing much
508        // inefficiency.  If a RegEx[] were passed (array, not a '...' list) and that array were
509        // changed after building this Predicate, the Predicate's behavior would fail.
510        // Avoid this (unlikely, but possible) problem by copying the array, and returning a 
511        // predicate that stores a different  array pointer (because it is to a different array)
512        // inside the Predicate's body.
513
514        final Pattern[] pArr = regExs.clone();
515
516        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once
517        // the lambda-predicate is invoked.
518
519        if (pArr.length == 0) throw new IllegalArgumentException(
520            "This static-factory method 'regExsNAND' has been invoked with zero-arguments to " +
521            "the 'regExs' var-args parameter."
522        );
523
524        return (Object o) ->
525        {
526            String s = o.toString();
527
528            // Cycle the Regular-Expressions.  "NAND" means if even one succeeds, return FALSE
529            // immediately.
530
531            for (Pattern p : pArr) if (p.matcher(s).find()) return false;
532
533            // All regex's failed, so return TRUE
534            return true;
535        };
536    }
537
538    /**
539     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE>
540     *
541     * The {@code Predicate} that is created by this factory-method will have a {@code test()}
542     * method that returns {@code TRUE} if an input object's {@code Object.toString()} output
543     * matches <I>precisely-one-of</I> the provided regular-expressions.
544     *
545     * @param regExs <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_REGEXS>
546     *
547     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
548     * according to the input-parameter {@code 'regExs'}
549     */
550    public static StrFilter regExsXOR(Pattern... regExs)
551    {
552        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once the
553        // lambda-predicate is invoked.
554
555        for (Pattern regEx : regExs)
556
557            if (regEx == null) throw new NullPointerException(
558                "One or more of the elements passed to static-factory method 'regExsXOR' are " +
559                "null."
560            );
561
562        // This over-comes a minor "possible complication" - without likely causing much
563        // inefficiency.  If a RegEx[] were passed (array, not a '...' list) and that array were
564        // changed after building this Predicate, the Predicate's behavior would fail.
565        // Avoid this (unlikely, but possible) problem by copying the array, and returning a 
566        // predicate that stores a different  array pointer (because it is to a different array)
567        // inside the Predicate's body.
568
569        final Pattern[] pArr = regExs.clone();
570
571        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once
572        // the lambda-predicate is invoked.
573
574        if (pArr.length == 0) throw new IllegalArgumentException(
575            "This static-factory method 'regExsXOR' has been invoked with zero-arguments to the " +
576            "'regExs' var-args parameter."
577        );
578
579        return (Object o) ->
580        {
581            String  s       = o.toString();
582            int     count   = 0;
583
584            // Cycle the Regular-Expressions.  Because this is "XOR" - we must keep a count.
585            // If that count is > 1, we have to return FALSE immediately.
586
587            for (Pattern p : pArr) if (p.matcher(s).find()) { if (++count > 1) return false; }
588
589            // If there was precisely one match, return TRUE, otherwise return FALSE.
590            return count == 1;
591        };
592    }
593
594
595    // ********************************************************************************************
596    // ********************************************************************************************
597    // Other static-factory methods
598    // ********************************************************************************************
599    // ********************************************************************************************
600
601
602    /**
603     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE2>
604     * 
605     * Here, the class {@code TextComparitor} is reused from the {@code 'NodeSearch'} package.
606     * Please review how each of the pre-defined, {@code static}-instances of class
607     * {@code TextComparitor} operate when presented with {@code String}-input.
608     *
609     * <DIV CLASS="EXAMPLE">{@code
610     * StrFilter filter = StrFilter.comparitor
611     *     (TextComparitor.DOES_NOT_START_WITH, "Today in Washington,");
612     * 
613     * // The above fiter would REJECT any input Object whose 'toString()' method returned a 
614     * // String that began with the words "Today in Washington,"
615     * 
616     * StrFilter filter2 = StrFilter.comparitor
617     *     (TextComparitor.CONTAINS_CASE_INSENSITIVE, "Highway 61 Revisited");
618     * 
619     * // This filter would KEEP / RETAIN any input Object whose 'toString()' method returned a
620     * // String that contained the words "Highway 61 Revisited".  This comparison to be performed
621     * // would be case-insensitive.
622     * }</DIV>
623     *
624     * @param tc This is an instance of the {@code class TextComparitor}, which is defined in the
625     * NodeSearch package.
626     *
627     * @param compareStrs These must be the comparison-{@code String's} used by class
628     * {@code 'TextComparitor'} for performing the comparisons.
629     *
630     * @return A new {@code StrFilter} that can be used to test and filter {@code String's},
631     * according to the input-parameters {@code 'tc'} and {@code 'compareStrs'}
632     *
633     * @see TextComparitor
634     */
635    public static StrFilter comparitor(TextComparitor tc, java.lang.String... compareStrs)
636    {
637        // FAIL-FAST: Check that the user-provided-parameters would not cause exceptions once
638        // the lambda-predicate is invoked.
639
640        if (tc == null) throw new NullPointerException
641            ("A null TextComparitor has been passed to static-factory method 'comparitorKeep'");
642
643        TCCompareStrException.check(compareStrs);
644
645        // Mostly, this is over-board, but it doesn't slow anything down much, and it does force
646        // users to think about Object-References and Parameter-Argument Marshaling.
647
648        final String[] cmpStrs = compareStrs.clone();
649
650        // Builds (and returns) a Predicate<Object> that re-uses the TextComparitor's
651        // 'test(String, String...)' method.
652
653        return (Object o) -> tc.test(o.toString(), cmpStrs);
654    }
655
656    /**
657     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE2>
658     * 
659     * Here, a single {@code String} is used as input.  The {@code Predicate} that is created by
660     * this factory-method  will have a {@code test()} method that returns {@code TRUE} only if the
661     * an input object's {@code 'toString()'} output <I>equals</I> the the input-parameter
662     * {@code String 's'}.
663     *
664     * @param s This is a {@code String} which will be used to test for equality in the generated
665     * {@code Predicate.test(Object)}
666     *
667     * @param ignoreCase When this is {@code TRUE}, the equality-test will ignore case when
668     * performing its comparisons.
669     *
670     * @return A new {@code StrFilter} that can be used to test and filter {@code Object's} via the
671     * {@code Object.toString()} method, and the input-parameter {@code 's'}
672     */
673    public static StrFilter isEqualKEEP(String s, boolean ignoreCase)
674    {
675        if (s == null) throw new NullPointerException(
676            "A null String has been passed to parameter 's' of static-factory method " +
677            "'isEqualKEEP'."
678        );
679
680        // Builds & returns a predicate based on the whether or not 'ignoreCase' is true or false.
681        return ignoreCase
682            ? (Object o) -> o.toString().equalsIgnoreCase(s)
683            : (Object o) -> o.toString().equals(s);
684    }
685
686    /**
687     * <EMBED CLASS='external-html' DATA-FILE-ID=STR_FILTER_NOTE2>
688     * 
689     * Here, a single {@code String} is used as input.  The {@code Predicate} that is created by
690     * this factory-method will have a {@code test()} method that returns {@code TRUE} only if an
691     * input {@code Object's} {@code Object.toString()} output <I>does-not-equal</I> the the
692     * input-parameter {@code String 's'}.
693     *
694     * @param s This is a {@code String} which will be used to test for equality in the generated
695     * {@code Predicate.test(Object)}.
696     * 
697     * @param ignoreCase When this is {@code TRUE}, the equality-test will ignore case when
698     * performing its comparisons.
699     *
700     * @return A new {@code StrFilter} that can be used to test and filter {@code Object's} via the
701     * {@code Object.toString()} method, and the input-parameter {@code 's'}
702     */
703    public static StrFilter isEqualREJECT(String s, boolean ignoreCase)
704    {
705        if (s == null) throw new NullPointerException(
706            "A null String has been passed to parameter 's' of static-factory method " +
707            "'isEqualREJECT'."
708        );
709
710        // Builds & returns a predicate based on the whether or not 'ignoreCase' is true or false.
711        return ignoreCase
712            ? (Object o) -> ! o.toString().equalsIgnoreCase(s)
713            : (Object o) -> ! o.toString().equals(s);
714    }
715}