All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.pojosontheweb.selenium.Findr Maven / Gradle / Ivy

There is a newer version: 3.0.1
Show newest version
package com.pojosontheweb.selenium;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.WebDriverWait;

/**
 * Utility for accessing Selenium DOM safely, wait-style.
 *
 * Allows to create chains of conditions and execute those conditions
 * inside a WebDriverWait, in a transparent fashion.
 *
 * Instances are immutable and can be reused safely.
 */
public final class Findr {

    /** the default wait timeout */
    public static final int WAIT_TIMEOUT_SECONDS = 10; // secs

    /** the sys prop name for enabling logs in findr eval(s) */
    public static final String SYSPROP_VERBOSE = "webtests.findr.verbose";

    private static final FindrActions DEFAULT_ACTIONS = new FindrActions();

    /** ref to the driver */
    private final WebDriver driver;

    /** the composed function */
    private final Function f;

    /**
     * A list of strings that represent the "path" for this findr,
     * used to create meaningful failure messages
     */
    private final List path;

    /**
     * The wait timeout (in seconds)
     */
    private final int waitTimeout;

    /**
     * The sleep interval (between polls)
     */
    private final long sleepInMillis;

    private final FindrActions findrActions;

    public static boolean isDebugEnabled() {
        return Boolean.valueOf(System.getProperty(SYSPROP_VERBOSE, "false"));
    }

    private static Function debugHandler = new Function() {
        @Override
        public Object apply(String input) {
            System.out.println(input);
            return null;
        }
    };

    /**
     * Pass a function that gets called-back with the logs. By default, logs
     * messages to stdout.
     * @param h the debug log handler function
     */
    public static void setDebugHandler(Function h) {
        debugHandler = h;
    }

    public static void logDebug(String message) {
        if (isDebugEnabled()) {
            debugHandler.apply(message);
        }
    }

    /**
     * Create a Findr with passed arguments
     * @param driver the WebDriver
     */
    public Findr(WebDriver driver) {
        this(driver, WAIT_TIMEOUT_SECONDS);
    }

    /**
     * Create a Findr with passed arguments
     * @param driver the WebDriver
     * @param waitTimeout the wait timeout in seconds
     */
    public Findr(WebDriver driver, int waitTimeout) {
        this(
                driver,
                waitTimeout,
                500,
                null,
                Collections.emptyList(),
                DEFAULT_ACTIONS
        );
    }

    /**
     * Return the web driver passed at construction time
     * @return the web driver
     */
    public WebDriver getDriver() {
        return driver;
    }

    /**
     * Return the timeout for this findr in seconds
     * @return the timeout in seconds
     */
    public int getTimeout() {
        return waitTimeout;
    }

    /**
     * Helper for "nested" Findrs. Allows to use a WebElement as the
     * root of a new Findr.
     * @param driver The WebDriver
     * @param webElement the WebElement to use as root
     * @return a new Findr that has the specified WebElement as its root
     */
    public static Findr fromWebElement(WebDriver driver, final WebElement webElement) {
        return fromWebElement(driver, webElement, WAIT_TIMEOUT_SECONDS);
    }

    /**
     * Helper for "nested" Findrs. Allows to use a WebElement as the
     * root of a new Findr.
     * @param driver The WebDriver
     * @param webElement the WebElement to use as root
     * @param waitTimeout the wait timeout in seconds
     * @return a new Findr that has the specified WebElement as its root
     */
    public static Findr fromWebElement(WebDriver driver, final WebElement webElement, int waitTimeout) {
        Findr f = new Findr(driver, waitTimeout);
        return f.compose(new Function() {
            @Override
            public WebElement apply(SearchContext input) {
                return webElement;
            }
        }, "fromWebElement(" + webElement + ")");
    }

    private Findr(WebDriver driver,
                  int waitTimeout,
                  long sleepInMillis,
                  Function f,
                  List path,
                  FindrActions actions) {
        this.driver = driver;
        this.waitTimeout = waitTimeout;
        this.sleepInMillis = sleepInMillis;
        this.f = f;
        this.path = path;
        this.findrActions = actions;
    }

    private  Function withoutWebDriverException(final Function function) {
        return new Function() {
            @Override
            public T apply(F input) {
                try {
                    return function.apply(input);
                } catch(WebDriverException e) {
                    // retry in case of exception
                    return null;
                }
            }
        };
    }

    private Findr compose(final Function function, final String pathElem) {
        final Function newFunction = withoutWebDriverException(function);
        ArrayList newPath = new ArrayList(path);
        if (pathElem!=null) {
            newPath.add(pathElem);
        }
        Function composed;
        if (f==null) {
            composed = new Function() {
                @Override
                public WebElement apply(SearchContext input) {
                    WebElement res = newFunction.apply(input);
                    if (res==null) {
                        logDebug("[Findr]  ! " + pathElem + " (null)");
                    } else {
                        logDebug("[Findr]  > " + pathElem + " : " + res);
                    }
                    return res;
                }
            };
        } else {
            composed = new Function() {
                @Override
                public WebElement apply(SearchContext input) {
                    WebElement res1 = f.apply(input);
                    if (res1==null) {
                        logDebug("[Findr]  - " + pathElem);
                        return null;
                    } else {
                        WebElement res2 = newFunction.apply(res1);
                        if (res2==null) {
                            logDebug("[Findr]  ! " + pathElem);
                        } else {
                            logDebug("[Findr]  > " + pathElem + " : " + res2);
                        }
                        return res2;
                    }
                }
            };
        }
        return new Findr(driver, waitTimeout, sleepInMillis, composed, newPath, findrActions);

    }

    /**
     * Set the timeout (in seconds) and return an updated Findr
     * @param timeoutInSeconds the timeout in seconds
     * @return an updated Findr instance
     */
    public Findr setTimeout(int timeoutInSeconds) {
        return new Findr(driver, timeoutInSeconds, sleepInMillis, f, path, findrActions);
    }

    /**
     * Set the WebDriverWait sleep interval (in ms). Use to control polling frequency.
     * @param sleepInMillis the sleep interval in milliseconds
     * @return an updated Findr instance
     */
    public Findr setSleepInMillis(long sleepInMillis) {
        return new Findr(driver, waitTimeout, sleepInMillis, f, path, findrActions);
    }

    public Findr setActions(FindrActions actions) {
        return new Findr(driver, waitTimeout, sleepInMillis, f, path, actions);
    }

    /**
     * Empty the condition chain. Use to create new Findrs with same settings but
     * a different condition chain.
     * @return a new empty Findr with other props untouched
     */
    public Findr empty() {
        return new Findr(driver, waitTimeout, sleepInMillis, null, path, findrActions);
    }

    /**
     * Adds specified single-element selector to the chain, and return a new Findr.
     * @param by the selector
     * @return a new Findr with updated condition chain
     */
    public Findr elem(final By by) {
        return compose(
                new Function() {
                    @Override
                    public WebElement apply(SearchContext input) {
                        if (input==null) {
                            return null;
                        }
                        try {
                            return input.findElement(by);
                        } catch(Exception e) {
                            return null;
                        }
                    }
                },
                by.toString()
        );
    }

    /**
     * Shortcut for elem(By.cssSelector(...)).
     * @param selector the css selector
     * @return a new Findr with updated chain
     */
    public Findr $(String selector) {
        return elem(By.cssSelector(selector));
    }

    /**
     * Adds specified multiple element selector to the chain, and return a new ListFindr.
     * @param by the selector
     * @return a new ListFindr with updated condition chain
     */
    public ListFindr elemList(By by) {
        return new ListFindr(by);
    }

    /**
     * Shortcut for elemList(By.cssSelector(selector))
     * @param selector the css selector
     * @return a new ListFindr with updated condition chain
     */
    public ListFindr $$(String selector) {
        return elemList(By.cssSelector(selector));
    }

    public ListFindr append(ListFindr lf) {
        return new ListFindr(lf.by, lf.filters, lf.checkers);
    }

    public Findr append(Findr f) {
        List newPath = new ArrayList(path!=null?path:new ArrayList());
        String sp;
        if (f.path!=null) {
            newPath.addAll(f.path);
            sp = f.path.stream().collect(Collectors.joining(", "));
        } else {
            sp = "";
        }
        return compose(f.f, "append[" + sp + "]");
    }

    private  T wrapWebDriverWait(final Function callback) throws TimeoutException {
        try {
            return new WebDriverWait(driver, waitTimeout, sleepInMillis).until(callback);
        } catch(TimeoutException e) {
            // failed to find element(s), build exception message
            // and re-throw exception
            StringBuilder sb = new StringBuilder();
            for (Iterator it = path.iterator(); it.hasNext(); ) {
                sb.append(it.next());
                if (it.hasNext()) {
                    sb.append("->");
                }
            }
            throw new TimeoutException("Timed out trying to find path=" + sb.toString() + ", callback=" + callback, e);
        }
    }

    /**
     * Evaluates this Findr, and invokes passed callback if the whole chain succeeds. Throws
     * a TimeoutException otherwise.
     * @param callback the callback to invoke (called if the whole chain of conditions succeeded)
     * @param  the return type of the callback
     * @return the result of the callback
     * @throws TimeoutException if at least one condition in the chain failed
     */
    public  T eval(final Function callback) throws TimeoutException {
        return wrapWebDriverWait(withoutWebDriverException(new Function() {
            @Override
            public T apply(WebDriver input) {
                if (f==null) {
                    throw new EmptyFindrException();
                }
                logDebug("[Findr] eval");
                WebElement e = f.apply(input);
                if (e == null) {
                    logDebug("[Findr]  => Chain STOPPED before callback");
                    return null;
                }
                T res = callback.apply(e);
                if (res==null || (res instanceof Boolean && !((Boolean)res))) {
                    logDebug("[Findr]  => " + callback + " result : " + res + ", will try again");
                } else {
                    logDebug("[Findr]  => " + callback + " result : " + res + ", OK");
                }
                return res;
            }
        }));
    }

    public static final Function IDENTITY_FOR_EVAL = new Function() {
        @Override
        public Object apply(WebElement webElement) {
            return true;
        }
    };

    /**
     * Evaluates this Findr, and blocks until all conditions are satisfied. Throws
     * a TimeoutException otherwise.
     */
    public void eval() throws TimeoutException {
        eval(IDENTITY_FOR_EVAL);
    }

    /**
     * Evaluates this Findr, and blocks until all conditions are satisfied. Throws
     * a TimeoutException otherwise.
     * @param failureMessage A message to be included to the timeout exception
     */
    public void eval(String failureMessage) throws TimeoutException {
        try {
            eval();
        } catch(TimeoutException e) {
            throw new TimeoutException(failureMessage, e);
        }
    }

    /**
     * Evaluates this Findr, and invokes passed callback if the whole chain succeeds. Throws
     * a TimeoutException with passed failure message otherwise.
     * @param callback the callback to invoke (called if the whole chain of conditions succeeded)
     * @param  the return type of the callback
     * @param failureMessage A message to be included to the timeout exception
     * @return the result of the callback
     * @throws TimeoutException if at least one condition in the chain failed
     */
    public  T eval(final Function callback, String failureMessage) throws TimeoutException {
        try {
            return eval(callback);
        } catch (TimeoutException e) {
            throw new TimeoutException(failureMessage, e);
        }
    }

    /**
     * Adds a Predicate (condition) to the chain, and return a new Findr
     * with updated chain.
     * @param predicate the condition to add
     * @return a Findr with updated conditions chain
     */
    public Findr where(final Predicate predicate) {
        return compose(new Function() {
            @Override
            public WebElement apply(SearchContext input) {
                if (input==null) {
                    return null;
                }
                if (input instanceof WebElement) {
                    WebElement webElement = (WebElement)input;
                    if (predicate.test(webElement)) {
                        return webElement;
                    }
                    return null;
                } else {
                    throw new RuntimeException("input is not a WebElement : " + input);
                }
            }
        },
                predicate.toString()
        );
    }

    /**
     * Shortcut method : evaluates chain, and sends keys to target WebElement of this
     * Findr. If sendKeys throws an exception, then the whole chain is evaluated again, until
     * no exception is thrown, or timeout.
     * @param keys the text to send
     * @throws TimeoutException if at least one condition in the chain failed
     */
    public void sendKeys(CharSequence... keys) throws TimeoutException {
        eval(findrActions.sendKeys(keys));
    }

    /**
     * Shortcut method : evaluates chain, and clicks target WebElement of this
     * Findr. If the click throws an exception, then the whole chain is evaluated again, until
     * no exception is thrown, or timeout.
     * @throws TimeoutException if at least one condition in the chain failed
     */
    public void click() {
        eval(findrActions.click());
    }

    /**
     * Shortcut method : evaluates chain, and clears target WebElement of this
     * Findr. If clear throws an exception, then the whole chain is evaluated again, until
     * no exception is thrown, or timeout.
     * @throws TimeoutException if at least one condition in the chain failed
     */
    public void clear() {
        eval(findrActions.clear());
    }

    private static final Function,Object> IDENTITY_LIST = new Function, Object>() {
        @Override
        public Object apply(List webElements) {
            return webElements;
        }
    };

    /**
     * Findr counterpart for element lists. Instances of this class are created and
     * returned by Findr.elemList(). Allows for index-based and filtering.
     */
    public class ListFindr {

        private final By by;
        private final Predicate filters;
        private final Predicate> checkers;

        private ListFindr(By by) {
            this(by, null, null);
        }

        private ListFindr(By by, Predicate filters, Predicate> checkers) {
            this.by = by;
            this.filters = filters;
            this.checkers = checkers;
        }

        private  Predicate wrapAndTrap(final Predicate predicate) {
            return (T input) -> {
                if (input==null) {
                    return false;
                }
                try {
                    return predicate.test(input);
                } catch(WebDriverException e) {
                    return false;
                }
            };
        }

        private  T wrapWebDriverWaitList(final Function callback) throws TimeoutException {
            try {
                return new WebDriverWait(driver, waitTimeout, sleepInMillis).until(callback);
            } catch(TimeoutException e) {
                // failed to find element(s), build exception message
                // and re-throw exception
                ArrayList newPath = new ArrayList(path);
                newPath.add(by.toString());
                StringBuilder sb = new StringBuilder();
                for (Iterator it = newPath.iterator(); it.hasNext(); ) {
                    sb.append(it.next());
                    if (it.hasNext()) {
                        sb.append("->");
                    }
                }
                throw new TimeoutException("Timed out trying to find path=" + sb.toString() + ", callback=" + callback, e);
            }
        }

        /**
         * Adds a filtering predicate, and returns a new ListFindr with updated chain.
         * @param predicate the predicate used for filtering the list of elements (applied on each element)
         * @return a new ListFindr with updated chain
         * @throws java.lang.IllegalArgumentException if called after whereElemCount.
         */
        public ListFindr where(final Predicate predicate) {
            if (checkers != null) {
                throw new IllegalArgumentException("It's forbidden to call ListFindr.where() after a whereXXX() method has been called.");
            }
            return new ListFindr(by, composeFilters(predicate), checkers);
        }

        private Predicate composeFilters(final Predicate predicate) {
            return new Predicate() {
                @Override
                public boolean test(WebElement input) {
                    return (filters == null || filters.test(input)) && wrapAndTrap(predicate).test(input);
                }

                @Override
                public String toString() {
                    if (filters!=null) {
                        return filters.toString() + " + " + predicate.toString();
                    } else {
                        return predicate.toString();
                    }
                }
            };
        }

        private Predicate> composeCheckers(final Predicate> predicate) {
            return new Predicate>() {
                @Override
                public boolean test(List input) {
                    return (checkers == null || checkers.test(input)) && wrapAndTrap(predicate).test(input);
                }

                @Override
                public String toString() {
                    if (filters!=null) {
                        return filters.toString() + " + " + predicate.toString();
                    } else {
                        return predicate.toString();
                    }
                }
            };
        }

        /**
         * Index-based access to the list of elements in this ListFindr. Allows
         * to wait for the n-th elem.
         * @param index the index of the element to wait for
         * @return a new Findr with updated chain
         */
        public Findr at(final int index) {
            return compose(new Function(){
                @Override
                public WebElement apply(SearchContext input) {
                    List elements;
                    List filtered;
                    try {
                        elements = input.findElements(by);
                        filtered = filterElements(elements);
                    } catch(Exception e) {
                        return null;
                    }
                    if (elements==null) {
                        return null;
                    }
                    if (checkers != null && !checkers.test(filtered)) {
                        logDebug("[Findr]  ! checkList KO: " + checkers);
                        logDebug("[Findr]  => Chain STOPPED before callback");
                        return null;
                    } else {
                        if (isDebugEnabled() && checkers!=null) {
                            logDebug("[Findr]  > checkList OK: " + checkers);
                        }
                    }
                    if (index>=filtered.size()) {
                        return null;
                    }
                    return filtered.get(index);
                }
            },
                    by.toString() + "[" + index + "]"
            );
        }

        private List filterElements(List source) {
            List filtered = new ArrayList();
            for (WebElement element : source) {
                if (filters==null || filters.test(element)) {
                    filtered.add(element);
                }
            }
            if (isDebugEnabled() && filters!=null) {
                int srcSize = source.size();
                int filteredSize = filtered.size();
                logDebug("[Findr]  > [" + by + "]* filter(" + filters + ") : " + srcSize + " -> " + filteredSize);
            }
            return filtered;
        }

        /**
         * Alias for count
         */
        public ListFindr whereElemCount(int elemCount) {
            return count(elemCount);
        }

        /**
         * Wait for the list findr to mach passed count
         * @param elemCount the expected count
         * @return a new ListFindr with updated chain
         */
        public ListFindr count(int elemCount) {
            return new ListFindr(by, filters, composeCheckers(checkElemCount(elemCount)));
        }

        /**
         * Wait for the list findr so that at least one of its element match the specified predicate. Check is OK if list is empty.
         * @param predicate the element predicate to match
         * @return a new ListFindr with updated chain
         */
        public ListFindr whereAny(final Predicate predicate) {
            return new ListFindr(by, filters, composeCheckers(checkAny(predicate)));
        }

        /**
         * Wait for the list findr so that all of its element match the specified predicate. Check is OK if list is empty.
         * @param predicate the element predicate to match
         * @return a new ListFindr with updated chain
         */
        public ListFindr whereAll(final Predicate predicate) {
            return new ListFindr(by, filters, composeCheckers(checkAll(predicate)));
        }

        private Predicate> checkAny(final Predicate predicate) {
            return new Predicate>() {
                @Override
                public boolean test(List elements) {
                    if (elements.size() == 0) {
                        return true;
                    }
                    for (WebElement element : elements) {
                        if (predicate.test(element)) {
                            return true;
                        }
                    }
                    return false;
                }

                @Override
                public String toString() {
                    return "any(" + predicate + ")";
                }
            };
        }

        private Predicate> checkAll(final Predicate predicate) {
            return new Predicate>() {
                @Override
                public boolean test(List elements) {
                    for (WebElement element : elements) {
                        if (!predicate.test(element)) {
                            return false;
                        }
                    }
                    return true;
                }

                @Override
                public String toString() {
                    return "all(" + predicate + ")";
                }
            };
        }

        private Predicate> checkElemCount(final int expectedCount) {
            return new Predicate>() {
                @Override
                public boolean test(List elements) {
                    return elements != null && elements.size() == expectedCount;
                }

                @Override
                public String toString() {
                    return "elemCount(" + expectedCount + ")";
                }
            };
        }

        /**
         * Evaluates this ListFindr and invokes passed callback if the whole chain suceeded. Throws
         * a TimeoutException if the condition chain didn't match.
         * @param callback the callback to call if the chain succeeds
         * @param  the rturn type of the callback
         * @return the result of the callback
         * @throws TimeoutException if at least one condition in the chain failed
         */
        public  T eval(final Function, T> callback) throws TimeoutException {
            logDebug("[Findr] ListFindr eval");
            return wrapWebDriverWaitList(withoutWebDriverException(new Function() {
                @Override
                public T apply(WebDriver input) {
                    SearchContext c = f == null ? input : f.apply(input);
                    if (c == null) {
                        return null;
                    }
                    List elements = c.findElements(by);
                    if (elements == null) {
                        return null;
                    }
                    List filtered = filterElements(elements);
                    if (checkers != null && !checkers.test(filtered)) {
                        logDebug("[Findr]  ! checkList KO: " + checkers);
                        logDebug("[Findr]  => Chain STOPPED before callback");
                        return null;
                    } else {
                        if (isDebugEnabled() && checkers!=null) {
                            logDebug("[Findr]  > checkList OK: " + checkers);
                        }
                    }
                    T res = callback.apply(filtered);
                    if (res==null || (res instanceof Boolean && !((Boolean)res))) {
                        logDebug("[Findr]  => " + callback + " result : " + res + ", will try again");
                    } else {
                        logDebug("[Findr]  => " + callback + " result : " + res + ", OK");
                    }
                    return res;
                }
            }));
        }

        /**
         * Evaluates this ListFindr. Throws
         * a TimeoutException if the condition chain didn't match.
         * @throws TimeoutException if at least one condition in the chain failed
         */
        public void eval() throws TimeoutException {
            eval(IDENTITY_LIST);
        }

        /**
         * Evaluates this ListFindr. Throws
         * a TimeoutException if the condition chain didn't match.
         * @param failureMessage A message to include in the timeout exception
         * @throws TimeoutException if at least one condition in the chain failed
         */
        public void eval(String failureMessage) throws TimeoutException {
            try {
                eval(IDENTITY_LIST);
            } catch(TimeoutException e) {
                throw new TimeoutException(failureMessage, e);
            }
        }

        /**
         * Evaluates this ListFindr and invokes passed callback if the whole chain suceeded. Throws
         * a TimeoutException with passed failure message if the condition chain didn't match.
         * @param callback the callback to call if the chain succeeds
         * @param  the rturn type of the callback
         * @return the result of the callback
         * @throws TimeoutException if at least one condition in the chain failed
         */
        public  T eval(Function, T> callback, String failureMessage) throws TimeoutException {
            try {
                return eval(callback);
            } catch(TimeoutException e) {
                throw new TimeoutException(failureMessage, e);
            }

        }

        @Override
        public String toString() {
            return "ListFindr{" +
                    "by=" + by +
                    ", filters=" + filters +
                    ", checkers=" + checkers +
                    ", findr=" + Findr.this +
                    '}';
        }
    }

    @Override
    public String toString() {
        return "Findr{" +
                "driver=" + driver +
                ", path=" + path +
                ", waitTimeout=" + waitTimeout +
                '}';
    }

    // Utility statics
    // ---------------

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate attrEquals(final String attrName, final String expectedValue) {
        return Findrs.attrEquals(attrName, expectedValue);
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate attrStartsWith(final String attrName, final String expectedStartsWith) {
        return Findrs.attrStartsWith(attrName, expectedStartsWith);
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate attrEndsWith(final String attrName, final String expectedEndsWith) {
        return Findrs.attrEndsWith(attrName, expectedEndsWith);
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate hasClass(final String className) {
        return Findrs.hasClass(className);
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate textEquals(final String expected) {
        return Findrs.textEquals(expected);
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate textStartsWith(final String expectedStartsWith) {
        return Findrs.textStartsWith(expectedStartsWith);
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate textEndsWith(final String expectedEndsWith) {
        return Findrs.textEndsWith(expectedEndsWith);
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate isEnabled() {
        return Findrs.isEnabled();
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate isDisplayed() {
        return Findrs.isDisplayed();
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate cssValue(final String propName, final String expectedValue) {
        return Findrs.cssValue(propName, expectedValue);
    }

    /**
     * @deprecated use Findrs.* instead
     */
    @Deprecated
    public static Predicate not(final Predicate in) {
        return Findrs.not(in);
    }

    public static final class EmptyFindrException extends IllegalStateException {
        public EmptyFindrException() {
            super("Calling eval() on an empty Findr ! You need to " +
                  "specify at least one condition before evaluating.");
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy