/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *******************************************************************************/
package org.apache.ofbiz.base.util;

import java.sql.Timestamp;
import java.util.Collection;
import java.util.Map;

import org.apache.commons.validator.routines.EmailValidator;
import org.apache.commons.validator.routines.UrlValidator;
import org.apache.ofbiz.base.lang.IsEmpty;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.entity.util.EntityUtilProperties;

import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import com.ibm.icu.util.Calendar;

/**
 * General input/data validation methods
 * Utility methods for validating data, especially input.
 * See detailed description below.
 *
 * <br> SUMMARY
 * <br>
 * <br> This is a set of meethods for validating input. Functions are provided to validate:
 * <br>    - U.S. and international phone/fax numbers
 * <br>    - U.S. ZIP codes(5 or 9 digit postal codes)
 * <br>    - U.S. Postal Codes(2 letter abbreviations for names of states)
 * <br>    - U.S. Social Security Numbers(abbreviated as SSNs)
 * <br>    - email addresses
 * <br>       - dates(entry of year, month, and day and validity of combined date)
 * <br>       - credit card numbers
 * <br>
 * <br> Supporting utility functions validate that:
 * <br>    - characters are Letter, Digit, or LetterOrDigit
 * <br>    - strings are a Signed, Positive, Negative, Nonpositive, or Nonnegative integer
 * <br>    - strings are a Float or a SignedFloat
 * <br>    - strings are Alphabetic, Alphanumeric, or Whitespace
 * <br>    - strings contain an integer within a specified range
 * <br>
 * <br> Other utility functions are provided to:
 * <br>    - remove from a string characters which are/are not in a "bag" of selected characters
 * <br>       - strip whitespace/leading whitespace from a string
 * <br>
 * <br> ==============================================================================
 * <br> NOTE: This code was adapted from the Netscape JavaScript form validation code,
 * <br> usually found in "FormChek.js". Credit card verification functions Originally
 * <br> included as Starter Application 1.0.0 in LivePayment.
 * <br> ==============================================================================
 */
public final class UtilValidate {

    private static final String MODULE = UtilValidate.class.getName();

    private UtilValidate() { }

    /** boolean specifying by default whether or not it is okay for a String to be empty */
    private static final boolean DEFAULT_EMPTY_OK = true;

    /** digit characters */
    public static final String DIGITS = "0123456789";

    /** lower-case letter characters */
    public static final String LOWER_CASE_LETTERS = "abcdefghijklmnopqrstuvwxyz";

    /** upper-case letter characters */
    public static final String UPPER_CASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    /** letter characters */
    public static final String LETTERS = LOWER_CASE_LETTERS + UPPER_CASE_LETTERS;

    /** whitespace characters */
    private static final String WHITESPACE = " \t\n\r";

    /** decimal point character differs by language and culture */
    public static final String DECIMAL_POINT_DELIMITER = ".";

    /** non-digit characters which are allowed in phone numbers */
    public static final String PHONE_NUMBER_DELIMITERS = "()- ";

    /** characters which are allowed in US phone numbers */
    public static final String VALID_US_PHONE_CHARS = DIGITS + PHONE_NUMBER_DELIMITERS;

    /** characters which are allowed in international phone numbers(a leading + is OK) */
    public static final String VALID_WORLD_PHONE_CHARS = DIGITS + PHONE_NUMBER_DELIMITERS + "+";

    /** non-digit characters which are allowed in Social Security Numbers */
    public static final String SSN_DELIMITERS = "- ";

    /** characters which are allowed in Social Security Numbers */
    public static final String VALID_SSN_CHARS = DIGITS + SSN_DELIMITERS;

    /** U.S. Social Security Numbers have 9 DIGITS. They are formatted as 123-45-6789. */
    public static final int DIGITS_IN_SSN = 9;

    /** U.S. phone numbers have 10 DIGITS. They are formatted as 123 456 7890 or(123) 456-7890. */
    public static final int DIGITS_IN_US_PHONE = 10;
    public static final int DIGITS_IN_US_PHONE_AREA = 3;
    public static final int DIGITS_IN_US_PHONE_MAIN = 7;

    /** non-digit characters which are allowed in ZIP Codes */
    public static final String ZIP_CODE_DELIMITERS = "-";

    /** our preferred delimiter for reformatting ZIP Codes */
    public static final String ZIP_CODE_DELIMITER = "-";

    /** characters which are allowed in Social Security Numbers */
    public static final String VALID_ZIP_CODE_CHARS = DIGITS + ZIP_CODE_DELIMITERS;

    /** U.S. ZIP codes have 5 or 9 digits. They are formatted as 12345 or 12345-6789. */
    public static final int DIGITS_IN_ZIP_CODE_1 = 5;

    /** U.S. ZIP codes have 5 or 9 digits. They are formatted as 12345 or 12345-6789. */
    public static final int DIGITS_IN_ZIP_CODE_2 = 9;

    /** non-digit characters which are allowed in credit card numbers */
    public static final String CREDIT_CARD_DELIMITERS = " -";

    /** An array of ints representing the number of days in each month of the year.
     *  Note: February varies depending on the year */
    static final int[] DAYS_IN_MONTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    /** Delimiter for USStateCodes String */
    public static final String US_STATE_CODE_DELIMITER = "|";

    /** Valid U.S. Postal Codes for states, territories, armed forces, etc.
     * See http://www.usps.gov/ncsc/lookups/abbr_state.txt. */
    public static final String US_STATE_CODES = "AL|AK|AS|AZ|AR|CA|CO|CT|DE|DC|FM|FL|GA|GU|HI|ID|IL|IN|IA|KS|KY|LA|ME|MH|MD|MA|MI|MN|MS|MO|MT|NE|NV|"
            + "NH|NJ|NM|NY|NC|ND|MP|OH|OK|OR|PW|PA|PR|RI|SC|SD|TN|TX|UT|VT|VI|VA|WA|WV|WI|WY|AE|AA|AE|AE|AP";

    /** Valid contiguous U.S. postal codes */
    public static final String CONTIGUOUS_US_STATE_CODES = "AL|AZ|AR|CA|CO|CT|DE|DC|FL|GA|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|"
            + "NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY";

    /** Check whether an object is empty, will see if it is a String, Map, Collection, etc. */
    public static boolean isEmpty(Object o) {
        return ObjectType.isEmpty(o);
    }

    /** Check whether an object is NOT empty, will see if it is a String, Map, Collection, etc. */
    public static boolean isNotEmpty(Object o) {
        return !ObjectType.isEmpty(o);
    }

    /** Check whether IsEmpty o is empty. */
    public static boolean isEmpty(IsEmpty o) {
        return o == null || o.isEmpty();
    }

    /** Check whether IsEmpty o is NOT empty. */
    public static boolean isNotEmpty(IsEmpty o) {
        return o != null && !o.isEmpty();
    }

    /** Check whether collection c is empty. */
    public static <E> boolean isEmpty(Collection<E> c) {
        return (c == null) || c.isEmpty();
    }

    /** Check whether map m is empty. */
    public static <K, E> boolean isEmpty(Map<K, E> m) {
        return (m == null) || m.isEmpty();
    }

    /** Check whether charsequence c is empty. */
    public static boolean isEmpty(CharSequence c) {
        return (c == null) || c.length() == 0;
    }

    /** Check whether collection c is NOT empty. */
    public static <E> boolean isNotEmpty(Collection<E> c) {
        return (c != null) && !c.isEmpty();
    }

    /** Check whether charsequence c is NOT empty. */
    public static boolean isNotEmpty(CharSequence c) {
        return ((c != null) && (c.length() > 0));
    }

    public static boolean isString(Object obj) {
        return ((obj != null) && (obj instanceof java.lang.String));
    }

    /** Returns true if string s is empty or whitespace characters only. */
    public static boolean isWhitespace(String s) {
        // Is s empty?
        if (isEmpty(s)) {
            return true;
        }

        // Search through string's characters one by one
        // until we find a non-whitespace character.
        // When we do, return false; if we don't, return true.
        for (int i = 0; i < s.length(); i++) {
            // Check that current character isn't whitespace.
            char c = s.charAt(i);

            if (WHITESPACE.indexOf(c) == -1) {
                return false;
            }
        }
        // All characters are whitespace.
        return true;
    }

    /** Removes all characters which appear in string bag from string s. */
    public static String stripCharsInBag(String s, String bag) {
        int i;
        StringBuilder stringBuilder = new StringBuilder("");

        // Search through string's characters one by one.
        // If character is not in bag, append to returnString.
        for (i = 0; i < s.length(); i++) {
            char c = s.charAt(i);

            if (bag.indexOf(c) == -1) {
                stringBuilder.append(c);
            }
        }
        return stringBuilder.toString();
    }

    /** Returns true if all characters in string s are numbers.
     *  Accepts non-signed integers only. Does not accept floating
     *  point, exponential notation, etc.
     */
    public static boolean isInteger(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        // Search through string's characters one by one
        // until we find a non-numeric character.
        // When we do, return false; if we don't, return true.
        for (int i = 0; i < s.length(); i++) {
            // Check that current character is number.
            char c = s.charAt(i);

            if (!Character.isDigit(c)) {
                return false;
            }
        }

        // All characters are numbers.
        return true;
    }

    /** Returns true if all characters are numbers;
     *  first character is allowed to be + or - as well.
     *  Does not accept floating point, exponential notation, etc.
     */
    public static boolean isSignedInteger(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        try {
            Integer.parseInt(s);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /** Returns true if all characters are numbers;
     *  first character is allowed to be + or - as well.
     *  Does not accept floating point, exponential notation, etc.
     */
    public static boolean isSignedLong(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        try {
            Long.parseLong(s);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Returns true if string s is an integer &gt; 0.
     * NOTE: using the Java Long object for greatest precision
     */
    public static boolean isPositiveInteger(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        try {
            long temp = Long.parseLong(s);
            return temp > 0;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Returns true if string s is an integer &gt;= 0
     */
    public static boolean isNonnegativeInteger(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        try {
            int temp = Integer.parseInt(s);
            return temp >= 0;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Returns true if string s is an integer &lt; 0
     */
    public static boolean isNegativeInteger(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        try {
            int temp = Integer.parseInt(s);
            return temp < 0;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Returns true if string s is an integer &lt;= 0
     */
    public static boolean isNonpositiveInteger(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        try {
            int temp = Integer.parseInt(s);
            return temp <= 0;
        } catch (Exception e) {
            return false;
        }
    }

    /** True if string s is an unsigned floating point(real) number.
     *  Also returns true for unsigned integers. If you wish
     *  to distinguish between integers and floating point numbers,
     *  first call isInteger, then call isFloat.
     *  Does not accept exponential notation.
     */
    public static boolean isFloat(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        boolean seenDecimalPoint = false;

        if (s.startsWith(DECIMAL_POINT_DELIMITER)) {
            return false;
        }

        // Search through string's characters one by one
        // until we find a non-numeric character.
        // When we do, return false; if we don't, return true.
        for (int i = 0; i < s.length(); i++) {
            // Check that current character is number.
            char c = s.charAt(i);

            if (c == DECIMAL_POINT_DELIMITER.charAt(0)) {
                if (!seenDecimalPoint) {
                    seenDecimalPoint = true;
                } else {
                    return false;
                }
            } else {
                if (!Character.isDigit(c)) {
                    return false;
                }
            }
        }
        // All characters are numbers.
        return true;
    }

    /** General routine for testing whether a string is a float.
     */
    public static boolean isFloat(String s, boolean allowNegative, boolean allowPositive, int minDecimal, int maxDecimal) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        try {
            float temp = Float.parseFloat(s);
            if ((!allowNegative && temp < 0) || (!allowPositive && temp > 0)) {
                return false;
            }
            int decimalPoint = s.indexOf(".");
            if (decimalPoint == -1) {
                return !(minDecimal > 0);
            }
            // 1.2345; length=6; point=1; num=4
            int numDecimals = s.length() - decimalPoint - 1;
            return !(minDecimal >= 0 && numDecimals < minDecimal) && !(maxDecimal >= 0 && numDecimals > maxDecimal);
        } catch (Exception e) {
            return false;
        }
    }

    /** General routine for testing whether a string is a double.
     */
    public static boolean isDouble(String s, boolean allowNegative, boolean allowPositive, int minDecimal, int maxDecimal) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        try {
            double temp = Double.parseDouble(s);
            if ((!allowNegative && temp < 0) || (!allowPositive && temp > 0)) {
                return false;
            }
            int decimalPoint = s.indexOf(".");
            if (decimalPoint == -1) {
                return !(minDecimal > 0);
            }
            // 1.2345; length=6; point=1; num=4
            int numDecimals = s.length() - decimalPoint - 1;
            return !(minDecimal >= 0 && numDecimals < minDecimal) && !(maxDecimal >= 0 && numDecimals > maxDecimal);
        } catch (Exception e) {
            return false;
        }
    }

    /** True if string s is a signed or unsigned floating point
     *  (real) number. First character is allowed to be + or -.
     *  Also returns true for unsigned integers. If you wish
     *  to distinguish between integers and floating point numbers,
     *  first call isSignedInteger, then call isSignedFloat.
     */
    public static boolean isSignedFloat(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        try {
            Float.parseFloat(s);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /** True if string s is a signed or unsigned floating point
     *  (real) number. First character is allowed to be + or -.
     *  Also returns true for unsigned integers. If you wish
     *  to distinguish between integers and floating point numbers,
     *  first call isSignedInteger, then call isSignedFloat.
     */
    public static boolean isSignedDouble(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        try {
            Double.parseDouble(s);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /** Returns true if string s is letters only.
     *  NOTE: This should handle i18n version to support European characters, etc.
     *  since it now uses Character.isLetter()
     */
    public static boolean isAlphabetic(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        // Search through string's characters one by one
        // until we find a non-alphabetic character.
        // When we do, return false; if we don't, return true.
        for (int i = 0; i < s.length(); i++) {
            // Check that current character is letter.
            char c = s.charAt(i);

            if (!Character.isLetter(c)) {
                return false;
            }
        }

        // All characters are letters.
        return true;
    }

    /** Returns true if string s is English letters (A .. Z, a..z) and numbers only.
     *  NOTE: Need i18n version to support European characters.
     *  This could be tricky due to different character
     *  sets and orderings for various languages and platforms.
     */
    public static boolean isAlphanumeric(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        // Search through string's characters one by one
        // until we find a non-alphanumeric character.
        // When we do, return false; if we don't, return true.
        for (int i = 0; i < s.length(); i++) {
            // Check that current character is number or letter.
            char c = s.charAt(i);

            if (!Character.isLetterOrDigit(c)) {
                return false;
            }
        }

        // All characters are numbers or letters.
        return true;
    }

    /* ================== METHODS TO CHECK VARIOUS FIELDS. ==================== */

    /** isSSN returns true if string s is a valid U.S. Social Security Number.  Must be 9 digits. */
    public static boolean isSSN(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        String normalizedSSN = stripCharsInBag(s, SSN_DELIMITERS);

        return (isInteger(normalizedSSN) && normalizedSSN.length() == DIGITS_IN_SSN);
    }

    /** isZIPCode returns true if string s is a valid U.S. ZIP code.  Must be 5 or 9 digits only. */
    private static boolean isZipCode(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }

        String normalizedZip = stripCharsInBag(s, ZIP_CODE_DELIMITERS);

        return (isInteger(normalizedZip) && ((normalizedZip.length() == DIGITS_IN_ZIP_CODE_1) || (normalizedZip.length() == DIGITS_IN_ZIP_CODE_2)));
    }

    /** Returns true if string s is a valid contiguous U.S. Zip code.  Must be 5 or 9 digits only. */
    public static boolean isContiguousZipCode(String s) {
        boolean retval = false;
        if (isZipCode(s)) {
            if (isEmpty(s)) {
                retval = DEFAULT_EMPTY_OK;
            } else {
                String normalizedZip = s.substring(0, 5);
                int iZip = Integer.parseInt(normalizedZip);
                if ((iZip >= 96701 && iZip <= 96898) || (iZip >= 99501 && iZip <= 99950)) {
                    retval = false;
                } else {
                    retval = true;
                }
            }
        }
        return retval;
    }

    /** Return true if s is a valid U.S. Postal Code (abbreviation for state). */
    public static boolean isStateCode(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return ((US_STATE_CODES.indexOf(s) != -1) && (s.indexOf(US_STATE_CODE_DELIMITER) == -1));
    }

    /** Return true if s is a valid contiguous U.S. Postal Code (abbreviation for state). */
    public static boolean isContiguousStateCode(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return ((CONTIGUOUS_US_STATE_CODES.indexOf(s) != -1) && (s.indexOf(US_STATE_CODE_DELIMITER) == -1));
    }

    public static boolean isEmail(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return EmailValidator.getInstance().isValid(s);
    }

    /**
     * Checks a String for a valid Email-List seperated by ",".
     */
    public static boolean isEmailList(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        String[] emails = s.split(",");
        for (String email : emails) {
            if (!EmailValidator.getInstance().isValid(email)) {
                return false;
            }
        }
        return true;
    }

    /**
     * isUrl returns true if the string contains ://
     * @param s String to validate Note: this does not handle "component://" specific to OFBiz
     * @return true if s contains ://
     */
    public static boolean isUrl(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return s.indexOf("://") != -1;
    }

    /**
     * urlInString returns true if the string contains :// and not "component://"
     * @param s String to validate
     * @return true if s contains :// and not "component://"
     */
    public static boolean urlInString(String s) {
        if (isEmpty(s) || s.contains("component://")) {
            return false;
        }
        return s.indexOf("://") != -1;
    }

    /**
     * isValidUrl returns true if the string is a valid URL (using Commons UrlValidator)
     * @param s String to validate
     * @return true if s contains if the string is a valid URL (using Commons UrlValidator)
     */
    public static boolean isValidUrl(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return UrlValidator.getInstance().isValid(s);
    }


    /** isYear returns true if string s is a valid
     *  Year number.  Must be 2 or 4 digits only.
     *  For Year 2000 compliance, you are advised
     *  to use 4-digit year numbers everywhere.
     */
    public static boolean isYear(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return isNonnegativeInteger(s) && ((s.length() == 2) || (s.length() == 4));
    }

    /** isIntegerInRange returns true if string s is an integer
     *  within the range of integer arguments a and b, inclusive.
     */
    public static boolean isIntegerInRange(String s, int a, int b) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        // Catch non-integer strings to avoid creating a NaN below,
        // which isn't available on JavaScript 1.0 for Windows.
        if (!isSignedInteger(s)) {
            return false;
        }
        // Now, explicitly change the type to integer via parseInt
        // so that the comparison code below will work both on
        // JavaScript 1.2(which typechecks in equality comparisons)
        // and JavaScript 1.1 and before(which doesn't).
        int num = Integer.parseInt(s);

        return ((num >= a) && (num <= b));
    }

    /** isMonth returns true if string s is a valid month number between 1 and 12. */
    public static boolean isMonth(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return isIntegerInRange(s, 1, 12);
    }

    /** isDay returns true if string s is a valid day number between 1 and 31. */
    public static boolean isDay(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return isIntegerInRange(s, 1, 31);
    }

    /** Given integer argument year, returns number of days in February of that year. */
    public static int daysInFebruary(int year) {
        // February has 29 days in any year evenly divisible by four,
        // EXCEPT for centurial years which are not also divisible by 400.
        return (((year % 4 == 0) && ((!(year % 100 == 0)) || (year % 400 == 0))) ? 29 : 28);
    }

    /** isHour returns true if string s is a valid number between 0 and 23. */
    public static boolean isHour(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return isIntegerInRange(s, 0, 23);
    }

    /** isMinute returns true if string s is a valid number between 0 and 59. */
    public static boolean isMinute(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return isIntegerInRange(s, 0, 59);
    }

    /** isSecond returns true if string s is a valid number between 0 and 59. */
    public static boolean isSecond(String s) {
        if (isEmpty(s)) {
            return DEFAULT_EMPTY_OK;
        }
        return isIntegerInRange(s, 0, 59);
    }

    /** isDate returns true if string arguments year, month, and day form a valid date. */
    public static boolean isDate(String year, String month, String day) {
        // catch invalid years(not 2- or 4-digit) and invalid months and days.
        if (!(isYear(year) && isMonth(month) && isDay(day))) {
            return false;
        }

        int intYear = Integer.parseInt(year);
        int intMonth = Integer.parseInt(month);
        int intDay = Integer.parseInt(day);

        // catch invalid days, except for February, so intDay > daysInMonth[intMonth - 1]
        return !(intDay > DAYS_IN_MONTH[intMonth - 1]) && !((intMonth == 2) && (intDay > daysInFebruary(intYear)));
    }

    /** isDate returns true if string argument date forms a valid date. */
    public static boolean isDate(String date) {
        if (isEmpty(date)) {
            return DEFAULT_EMPTY_OK;
        }
        String month;
        String day;
        String year;

        int dateSlash1 = date.indexOf("/");
        int dateSlash2 = date.lastIndexOf("/");

        if (dateSlash1 <= 0 || dateSlash1 == dateSlash2) {
            return false;
        }
        month = date.substring(0, dateSlash1);
        day = date.substring(dateSlash1 + 1, dateSlash2);
        year = date.substring(dateSlash2 + 1);

        return isDate(year, month, day);
    }

    /** isDate returns true if string argument date forms a valid date and is after today. */
    public static boolean isDateAfterToday(String date) {
        if (isEmpty(date)) {
            return DEFAULT_EMPTY_OK;
        }
        int dateSlash1 = date.indexOf("/");
        int dateSlash2 = date.lastIndexOf("/");

        if (dateSlash1 <= 0) {
            return false;
        }

        java.util.Date passed = null;
        if (dateSlash1 == dateSlash2) {
            // consider the day to be optional; use the first day of the following month for comparison since this is an is after test
            String month = date.substring(0, dateSlash1);
            String day = "28";
            String year = date.substring(dateSlash1 + 1);
            if (!isDate(year, month, day)) {
                return false;
            }

            try {
                int monthInt = Integer.parseInt(month);
                int yearInt = Integer.parseInt(year);
                Calendar calendar = Calendar.getInstance();
                calendar.set(yearInt, monthInt - 1, 0, 0, 0, 0);
                calendar.add(Calendar.MONTH, 1);
                passed = new java.util.Date(calendar.getTime().getTime());
            } catch (NumberFormatException e) {
                passed = null;
            }
        } else {
            String month = date.substring(0, dateSlash1);
            String day = date.substring(dateSlash1 + 1, dateSlash2);
            String year = date.substring(dateSlash2 + 1);
            if (!isDate(year, month, day)) {
                return false;
            }
            passed = UtilDateTime.toDate(month, day, year, "0", "0", "0");
        }

        java.util.Date now = UtilDateTime.nowDate();
        if (passed != null) {
            return passed.after(now);
        }
        return false;
    }

    /** isDate returns true if string argument date forms a valid date and is before today. */
    public static boolean isDateBeforeToday(String date) {
        if (isEmpty(date)) {
            return DEFAULT_EMPTY_OK;
        }
        int dateSlash1 = date.indexOf("/");
        int dateSlash2 = date.lastIndexOf("/");

        if (dateSlash1 <= 0) {
            return DEFAULT_EMPTY_OK; // In this case an issue number has been provided (requires a javascript check in template!)
        }

        java.util.Date passed = null;
        if (dateSlash1 == dateSlash2) {
            // consider the day to be optional; use the first day of the following month for comparison since this is an is after test
            String month = date.substring(0, dateSlash1);
            String day = "28";
            String year = date.substring(dateSlash1 + 1);
            if (!isDate(year, month, day)) {
                return false;
            }

            try {
                int monthInt = Integer.parseInt(month);
                int yearInt = Integer.parseInt(year);
                Calendar calendar = Calendar.getInstance();
                calendar.set(yearInt, monthInt - 1, 0, 0, 0, 0);
                calendar.add(Calendar.MONTH, 1);
                passed = new java.util.Date(calendar.getTime().getTime());
            } catch (NumberFormatException e) {
                passed = null;
            }
        } else {
            String month = date.substring(0, dateSlash1);
            String day = date.substring(dateSlash1 + 1, dateSlash2);
            String year = date.substring(dateSlash2 + 1);
            if (!isDate(year, month, day)) {
                return false;
            }
            passed = UtilDateTime.toDate(month, day, year, "0", "0", "0");
        }

        java.util.Date now = UtilDateTime.nowDate();
        if (passed != null) {
            return passed.before(now);
        }
        return false;
    }

    public static boolean isDateBeforeNow(Timestamp date) {
        Timestamp now = UtilDateTime.nowTimestamp();
        if (date != null) {
            return date.before(now);
        }
        return false;
    }

    public static boolean isDateAfterNow(Timestamp date) {
        Timestamp now = UtilDateTime.nowTimestamp();
        if (date != null) {
            return date.after(now);
        }
        return false;
    }
    /** isTime returns true if string arguments hour, minute, and second form a valid time. */
    public static boolean isTime(String hour, String minute, String second) {
        // catch invalid years(not 2- or 4-digit) and invalid months and days.
        return isHour(hour) && isMinute(minute) && isSecond(second);
    }

    /** isTime returns true if string argument time forms a valid time. */
    public static boolean isTime(String time) {
        if (isEmpty(time)) {
            return DEFAULT_EMPTY_OK;
        }

        String hour;
        String minute;
        String second;

        int timeColon1 = time.indexOf(":");
        int timeColon2 = time.lastIndexOf(":");

        if (timeColon1 <= 0) {
            return false;
        }
        hour = time.substring(0, timeColon1);
        if (timeColon1 == timeColon2) {
            minute = time.substring(timeColon1 + 1);
            second = "0";
        } else {
            minute = time.substring(timeColon1 + 1, timeColon2);
            second = time.substring(timeColon2 + 1);
        }
        return isTime(hour, minute, second);
    }

    /** Check to see if a card number is a valid ValueLink Gift Card
     * @param stPassed a string representing a valuelink gift card
     * @return true, if the number passed simple checks
     */
    public static boolean isValueLinkCard(String stPassed) {
        if (isEmpty(stPassed)) {
            return DEFAULT_EMPTY_OK;
        }
        String st = stripCharsInBag(stPassed, CREDIT_CARD_DELIMITERS);
        return st.length() == 16 && (st.startsWith("7") || st.startsWith("6"));
    }

    /** Check to see if a card number is a valid OFB Gift Card (Certifiicate)
     * @param stPassed a string representing a gift card
     * @return tru, if the number passed simple checks
     */
    public static boolean isOFBGiftCard(String stPassed) {
        if (isEmpty(stPassed)) {
            return DEFAULT_EMPTY_OK;
        }
        String st = stripCharsInBag(stPassed, CREDIT_CARD_DELIMITERS);
        return st.length() == 15 && sumIsMod10(getLuhnSum(st));
    }

    /** Check to see if a card number is a supported Gift Card
     * @param stPassed a string representing a gift card
     * @return true, if the number passed simple checks
     */
    public static boolean isGiftCard(String stPassed) {
        return isOFBGiftCard(stPassed) || isValueLinkCard(stPassed);
    }

    public static int getLuhnSum(String stPassed) {
        stPassed = stPassed.replaceAll("\\D", ""); // nuke any non-digit characters

        int len = stPassed.length();
        int sum = 0;
        int mul = 1;
        for (int i = len - 1; i >= 0; i--) {
            int digit = Character.digit(stPassed.charAt(i), 10);
            digit *= (mul == 1) ? mul++ : mul--;
            sum += (digit >= 10) ? (digit % 10) + 1 : digit;
        }

        return sum;
    }

    public static int getLuhnCheckDigit(String stPassed) {
        int sum = getLuhnSum(stPassed);
        int mod = ((sum / 10 + 1) * 10 - sum) % 10;
        return (10 - mod);
    }

    public static boolean sumIsMod10(int sum) {
        return ((sum % 10) == 0);
    }

    public static String appendCheckDigit(String stPassed) {
        String checkDigit = Integer.toString(getLuhnCheckDigit(stPassed));
        return stPassed + checkDigit;
    }

    /** Checks credit card number with Luhn Mod-10 test
     * @param stPassed a string representing a credit card number
     * @return true, if the credit card number passes the Luhn Mod-10 test, false otherwise
     */
    public static boolean isCreditCard(String stPassed) {
        if (isEmpty(stPassed)) {
            return DEFAULT_EMPTY_OK;
        }
        String st = stripCharsInBag(stPassed, CREDIT_CARD_DELIMITERS);

        // encoding only works on cars with less the 19 digits, so st.length() > 19
        return isInteger(st) && !(st.length() > 19) && sumIsMod10(getLuhnSum(st));
    }

    /** Checks to see if the cc number is a valid Visa number
     * @param cc a string representing a credit card number; Sample number: 4111 1111 1111 1111(16 digits)
     * @return true, if the credit card number is a valid VISA number, false otherwise
     */
    public static boolean isVisa(String cc) {
        if (((cc.length() == 16) || (cc.length() == 13)) && ("4".equals(cc.substring(0, 1)))) {
            return isCreditCard(cc);
        }
        return false;
    }

    /** Checks to see if the cc number is a valid Master Card number
     * @param cc a string representing a credit card number; MasterCard numbers either start with the numbers 51 through 55 or with the numbers 2221
     *           through 2720. All have 16 digits; Sample number: 5500 0000 0000 0004(16 digits)
     * @return true, if the credit card number is a valid MasterCard  number, false otherwise
     */
    public static boolean isMasterCard(String cc) {
        int first2digs = Integer.parseInt(cc.substring(0, 2));
        int first4digs = Integer.parseInt(cc.substring(0, 4));

        if ((Integer.compare(cc.length(), 16) == 0) && ((first2digs >= 51 && first2digs <= 55) || (first4digs >= 2221 && first4digs <= 2720))) {
            return isCreditCard(cc);
        }
        return false;

    }

    /** Checks to see if the cc number is a valid American Express number
     *   @param    cc - a string representing a credit card number; Sample number: 340000000000009(15 digits)
     *   @return  true, if the credit card number is a valid American Express number, false otherwise
     */
    public static boolean isAmericanExpress(String cc) {
        int firstdig = Integer.parseInt(cc.substring(0, 1));
        int seconddig = Integer.parseInt(cc.substring(1, 2));

        if ((cc.length() == 15) && (firstdig == 3) && ((seconddig == 4) || (seconddig == 7))) {
            return isCreditCard(cc);
        }
        return false;

    }

    /** Checks to see if the cc number is a valid Diners Club number
     *   @param    cc - a string representing a credit card number; Sample number: 30000000000004(14 digits)
     *   @return  true, if the credit card number is a valid Diner's Club number, false otherwise
     */
    public static boolean isDinersClub(String cc) {
        int firstdig = Integer.parseInt(cc.substring(0, 1));
        int seconddig = Integer.parseInt(cc.substring(1, 2));

        if ((cc.length() == 14) && (firstdig == 3) && ((seconddig == 0) || (seconddig == 6) || (seconddig == 8))) {
            return isCreditCard(cc);
        }
        return false;
    }

    /** Checks to see if the cc number is a valid Carte Blanche number
     *   @param    cc - a string representing a credit card number; Sample number: 30000000000004(14 digits)
     *   @return  true, if the credit card number is a valid Carte Blanche number, false otherwise
     */
    public static boolean isCarteBlanche(String cc) {
        return isDinersClub(cc);
    }

    /** Checks to see if the cc number is a valid Discover number
     *   @param    cc - a string representing a credit card number; Discover card numbers begin with 6011 or 65. All have 16 digits;
     *                Sample number: 6011000000000004(16 digits)
     *   @return  true, if the credit card number is a valid Discover card number, false otherwise
     */
    public static boolean isDiscover(String cc) {
        String first4digs = cc.substring(0, 4);
        String first2digs = cc.substring(0, 2);

        if ((Integer.compare(cc.length(), 16) == 0) && ("6011".equals(first4digs) || "65".equals(first2digs))) {
            return isCreditCard(cc);
        }
        return false;
    }

    /** Checks to see if the cc number is a valid EnRoute number
     *   @param    cc - a string representing a credit card number; Sample number: 201400000000009(15 digits)
     *   @return  true, if the credit card number is a valid enRoute card number, false, otherwise
     */
    public static boolean isEnRoute(String cc) {
        String first4digs = cc.substring(0, 4);

        if ((cc.length() == 15) && ("2014".equals(first4digs) || "2149".equals(first4digs))) {
            return isCreditCard(cc);
        }
        return false;
    }

    /** Checks to see if the cc number is a valid JCB number
     *   @param     cc - a string representing a credit card number; JCB cards beginning with 2131 or 1800 have 15 digits. JCB cards beginning
     *                 with 35 have 16 digits;Sample number: 3088000000000009(16 digits)
     *   @return  true, if the credit card number is a valid JCB card number, false otherwise
     */
    public static boolean isJCB(String cc) {
        String first4digs = cc.substring(0, 4);
        String first2digs = cc.substring(0, 2);

        if (((Integer.compare(cc.length(), 16) == 0) && "35".equals(first2digs)) || ((Integer.compare(cc.length(), 15) == 0)
                && ("2131".equals(first4digs) || "1800".equals(first4digs)))) {
            return isCreditCard(cc);
        }
        return false;
    }

    /** Checks to see if the cc number is a valid Switch number
     *   @param     cc - a string representing a credit card number; Sample number: 6331100000000096(16 digits)
     *   @return  true, if the credit card number is a valid Switch card number, false otherwise
     */
    public static boolean isSwitch(String cc) {
        String first4digs = cc.substring(0, 4);
        String first6digs = cc.substring(0, 6);

        if (((cc.length() == 16) || (cc.length() == 18) || (cc.length() == 19))
                && ("4903".equals(first4digs)
                || "4905".equals(first4digs)
                || "4911".equals(first4digs)
                || "4936".equals(first4digs)
                || "564182".equals(first6digs)
                || "633110".equals(first6digs)
                || "6333".equals(first4digs)
                || "6759".equals(first4digs))) {
            return isCreditCard(cc);
        }
        return false;
    }

    /** Checks to see if the cc number is a valid Solo number
     *   @param     cc - a string representing a credit card number; Sample number: 6331100000000096 (16 digits)
     *   @return  true, if the credit card number is a valid Solo card number, false otherwise
     */
    public static boolean isSolo(String cc) {
        String first4digs = cc.substring(0, 4);
        String first2digs = cc.substring(0, 2);
        if (((cc.length() == 16) || (cc.length() == 18) || (cc.length() == 19)) && ("63".equals(first2digs) || "6767".equals(first4digs))) {
            return isCreditCard(cc);
        }
        return false;
    }

    /** Checks to see if the cc number is a valid Visa Electron number
     *   @param    cc - a string representing a credit card number; Sample number: 4175000000000001(16 digits)
     *   @return  true, if the credit card number is a valid Visa Electron card number, false otherwise
     */
    public static boolean isVisaElectron(String cc) {
        String first6digs = cc.substring(0, 6);
        String first4digs = cc.substring(0, 4);

        if ((cc.length() == 16) && ("417500".equals(first6digs)
                || "4917".equals(first4digs)
                || "4913".equals(first4digs)
                || "4508".equals(first4digs)
                || "4844".equals(first4digs)
                || "4027".equals(first4digs))) {
            return isCreditCard(cc);
        }
        return false;
    }

    /** Checks to see if the cc number is a valid number for any accepted credit card
     *   @param     ccPassed - a string representing a credit card number
     *   @return  true, if the credit card number is any valid credit card number for any of the accepted card types, false otherwise
     */
    public static boolean isAnyCard(String ccPassed) {
        if (isEmpty(ccPassed)) {
            return DEFAULT_EMPTY_OK;
        }
        String cc = stripCharsInBag(ccPassed, CREDIT_CARD_DELIMITERS);
        return isCreditCard(cc) && (isMasterCard(cc) || isVisa(cc) || isAmericanExpress(cc) || isDinersClub(cc)
                        || isDiscover(cc) || isEnRoute(cc) || isJCB(cc) || isSolo(cc) || isSwitch(cc) || isVisaElectron(cc));
    }

    /** Checks to see if the cc number is a valid number for any accepted credit card, and return the name of that type
     *   @param     ccPassed - a string representing a credit card number
     *   @return  true, if the credit card number is any valid credit card number for any of the accepted card types, false otherwise
     */
    public static String getCardType(String ccPassed) {
        if (isEmpty(ccPassed)) {
            return "Unknown";
        }
        String cc = stripCharsInBag(ccPassed, CREDIT_CARD_DELIMITERS);

        if (!isCreditCard(cc)) {
            return "Unknown";
        }

        if (isMasterCard(cc)) {
            return "CCT_MASTERCARD";
        }
        if (isVisa(cc)) {
            return "CCT_VISA";
        }
        if (isAmericanExpress(cc)) {
            return "CCT_AMERICANEXPRESS";
        }
        if (isDinersClub(cc)) {
            return "CCT_DINERSCLUB";
        }
        if (isDiscover(cc)) {
            return "CCT_DISCOVER";
        }
        if (isEnRoute(cc)) {
            return "CCT_ENROUTE";
        }
        if (isJCB(cc)) {
            return "CCT_JCB";
        }
        if (isSolo(cc)) {
            return "CCT_SOLO";
        }
        if (isSwitch(cc)) {
            return "CCT_SWITCH";
        }
        if (isVisaElectron(cc)) {
            return "CCT_VISAELECTRON";
        }
        return "Unknown";
    }

    /** Checks to see if the cc number is a valid number for the specified type
     *   @param    cardType - a string representing the credit card type
     *   @param    cardNumberPassed - a string representing a credit card number
     *   @return  true, if the credit card number is valid for the particular credit card type given in "cardType", false otherwise
     */
    public static boolean isCardMatch(String cardType, String cardNumberPassed) {
        if (isEmpty(cardType) || isEmpty(cardNumberPassed)) {
            return DEFAULT_EMPTY_OK;
        }
        String cardNumber = stripCharsInBag(cardNumberPassed, CREDIT_CARD_DELIMITERS);

        if (("CCT_VISA".equalsIgnoreCase(cardType)) && (isVisa(cardNumber))) {
            return true;
        }
        if (("CCT_MASTERCARD".equalsIgnoreCase(cardType)) && (isMasterCard(cardNumber))) {
            return true;
        }
        if ((("CCT_AMERICANEXPRESS".equalsIgnoreCase(cardType)) || ("CCT_AMEX".equalsIgnoreCase(cardType))) && (isAmericanExpress(cardNumber))) {
            return true;
        }
        if (("CCT_DISCOVER".equalsIgnoreCase(cardType)) && (isDiscover(cardNumber))) {
            return true;
        }
        if (("CCT_JCB".equalsIgnoreCase(cardType)) && (isJCB(cardNumber))) {
            return true;
        }
        if ((("CCT_DINERSCLUB".equalsIgnoreCase(cardType)) || ("CCT_DINERS".equalsIgnoreCase(cardType))) && (isDinersClub(cardNumber))) {
            return true;
        }
        if (("CCT_CARTEBLANCHE".equalsIgnoreCase(cardType)) && (isCarteBlanche(cardNumber))) {
            return true;
        }
        if (("CCT_ENROUTE".equalsIgnoreCase(cardType)) && (isEnRoute(cardNumber))) {
            return true;
        }
        if (("CCT_SOLO".equalsIgnoreCase(cardType)) && (isSolo(cardNumber))) {
            return true;
        }
        if (("CCT_SWITCH".equalsIgnoreCase(cardType)) && (isSwitch(cardNumber))) {
            return true;
        }
        if (("CCT_VISAELECTRON".equalsIgnoreCase(cardType)) && (isVisaElectron(cardNumber))) {
            return true;
        }
        return false;
    }

    public static String checkValidDatabaseId(String fieldStr) {
        if (fieldStr.indexOf(' ') >= 0) {
            return "[space found at position " + (fieldStr.indexOf(' ') + 1) + "]";
        }
        if (fieldStr.indexOf('"') >= 0) {
            return "[double-quote found at position " + (fieldStr.indexOf('"') + 1) + "]";
        }
        if (fieldStr.indexOf('\'') >= 0) {
            return "[single-quote found at position " + (fieldStr.indexOf('\'') + 1) + "]";
        }
        if (fieldStr.indexOf('&') >= 0) {
            return "[ampersand found at position " + (fieldStr.indexOf('&') + 1) + "]";
        }
        if (fieldStr.indexOf('?') >= 0) {
            return "[question mark found at position " + (fieldStr.indexOf('?') + 1) + "]";
        }
        if (fieldStr.indexOf('<') >= 0) {
            return "[less-than sign found at position " + (fieldStr.indexOf('<') + 1) + "]";
        }
        if (fieldStr.indexOf('>') >= 0) {
            return "[greater-than sign found at position " + (fieldStr.indexOf('>') + 1) + "]";
        }
        if (fieldStr.indexOf('\\') >= 0) {
            return "[back-slash found at position " + (fieldStr.indexOf('\\') + 1) + "]";
        }
        if (fieldStr.indexOf('/') >= 0) {
            return "[forward-slash found at position " + (fieldStr.indexOf('/') + 1) + "]";
        }
        return null;
    }

    public static boolean isValidDatabaseId(String fieldStr, StringBuffer errorDetails) {
        boolean isValid = true;
        String checkMessage = checkValidDatabaseId(fieldStr);
        if (checkMessage != null) {
            isValid = false;
            errorDetails.append(checkMessage);
        }
        return isValid;
    }

    public static boolean isValidPhoneNumber(String phoneNumber, Delegator delegator) {
        String geoId = EntityUtilProperties.getPropertyValue("general", "country.geo.id.default", delegator);
        return isValidPhoneNumber(phoneNumber, geoId, delegator);
    }

    public static boolean isValidPhoneNumber(String phoneNumber, String geoId, Delegator delegator) {
        boolean isValid = false;
        try {
            GenericValue geo = EntityQuery.use(delegator).from("Geo").where("geoId", geoId).cache().queryOne();
            PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
            String geoCode = geo != null ? geo.getString("geoCode") : "US";
            PhoneNumber phNumber = phoneUtil.parse(phoneNumber, geoCode);
            if (phoneUtil.isValidNumber(phNumber) || phoneUtil.isPossibleNumber(phNumber)) {
                isValid = true;
            }
        } catch (GenericEntityException | NumberParseException ex) {
            Debug.logError(ex, MODULE);
        }
        return isValid;
    }
}
