ISINValidator.java

/*
 * 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.commons.validator.routines;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Locale;

import org.apache.commons.validator.routines.checkdigit.ISINCheckDigit;

/**
 * <b>ISIN</b> (International Securities Identifying Number) validation.
 *
 * <p>
 * ISIN Numbers are 12 character alphanumeric codes used to identify Securities.
 * </p>
 *
 * <p>
 * ISINs consist of two alphabetic characters,
 * which are the ISO 3166-1 alpha-2 code for the issuing country,
 * nine alpha-numeric characters (the National Securities Identifying Number, or NSIN, which identifies the security),
 * and one numerical check digit.
 * They are 12 characters in length.
 * </p>
 *
 * <p>
 * See <a href="https://en.wikipedia.org/wiki/ISIN">Wikipedia - ISIN</a>
 * for more details.
 * </p>
 *
 * @since 1.7
 */
public class ISINValidator implements Serializable {

    private static final long serialVersionUID = -5964391439144260936L;

    private static final String ISIN_REGEX = "([A-Z]{2}[A-Z0-9]{9}[0-9])";

    private static final CodeValidator VALIDATOR = new CodeValidator(ISIN_REGEX, 12, ISINCheckDigit.ISIN_CHECK_DIGIT);

    /** ISIN Code Validator (no countryCode check) */
    private static final ISINValidator ISIN_VALIDATOR_FALSE = new ISINValidator(false);

    /** ISIN Code Validator (with countryCode check) */
    private static final ISINValidator ISIN_VALIDATOR_TRUE = new ISINValidator(true);

    private static final String [] CCODES = Locale.getISOCountries();

    /**
     * All codes from ISO 3166-1 alpha-2 except unassigned code elements.
     *
     * From https://www.iso.org/obp/ui/#iso:pub:PUB500001:en as of 2024-03-23.
     */
    private static final String[] SPECIALS = {
            "AA",
            "AC",
            "AD",
            "AE",
            "AF",
            "AG",
            "AI",
            "AL",
            "AM",
            "AN",
            "AO",
            "AP",
            "AQ",
            "AR",
            "AS",
            "AT",
            "AU",
            "AW",
            "AX",
            "AZ",
            "BA",
            "BB",
            "BD",
            "BE",
            "BF",
            "BG",
            "BH",
            "BI",
            "BJ",
            "BL",
            "BM",
            "BN",
            "BO",
            "BQ",
            "BR",
            "BS",
            "BT",
            "BU",
            "BV",
            "BW",
            "BX",
            "BY",
            "BZ",
            "CA",
            "CC",
            "CD",
            "CF",
            "CG",
            "CH",
            "CI",
            "CK",
            "CL",
            "CM",
            "CN",
            "CO",
            "CP",
            "CQ",
            "CR",
            "CS",
            "CT",
            "CU",
            "CV",
            "CW",
            "CX",
            "CY",
            "CZ",
            "DD",
            "DE",
            "DG",
            "DJ",
            "DK",
            "DM",
            "DO",
            "DY",
            "DZ",
            "EA",
            "EC",
            "EE",
            "EF",
            "EG",
            "EH",
            "EM",
            "EP",
            "ER",
            "ES",
            "ET",
            "EU",
            "EV",
            "EW",
            "EZ",
            "FI",
            "FJ",
            "FK",
            "FL",
            "FM",
            "FO",
            "FQ",
            "FR",
            "FX",
            "GA",
            "GB",
            "GC",
            "GD",
            "GE",
            "GF",
            "GG",
            "GH",
            "GI",
            "GL",
            "GM",
            "GN",
            "GP",
            "GQ",
            "GR",
            "GS",
            "GT",
            "GU",
            "GW",
            "GY",
            "HK",
            "HM",
            "HN",
            "HR",
            "HT",
            "HU",
            "HV",
            "IB",
            "IC",
            "ID",
            "IE",
            "IL",
            "IM",
            "IN",
            "IO",
            "IQ",
            "IR",
            "IS",
            "IT",
            "JA",
            "JE",
            "JM",
            "JO",
            "JP",
            "JT",
            "KE",
            "KG",
            "KH",
            "KI",
            "KM",
            "KN",
            "KP",
            "KR",
            "KW",
            "KY",
            "KZ",
            "LA",
            "LB",
            "LC",
            "LF",
            "LI",
            "LK",
            "LR",
            "LS",
            "LT",
            "LU",
            "LV",
            "LY",
            "MA",
            "MC",
            "MD",
            "ME",
            "MF",
            "MG",
            "MH",
            "MI",
            "MK",
            "ML",
            "MM",
            "MN",
            "MO",
            "MP",
            "MQ",
            "MR",
            "MS",
            "MT",
            "MU",
            "MV",
            "MW",
            "MX",
            "MY",
            "MZ",
            "NA",
            "NC",
            "NE",
            "NF",
            "NG",
            "NH",
            "NI",
            "NL",
            "NO",
            "NP",
            "NQ",
            "NR",
            "NT",
            "NU",
            "NZ",
            "OA",
            "OM",
            "PA",
            "PC",
            "PE",
            "PF",
            "PG",
            "PH",
            "PI",
            "PK",
            "PL",
            "PM",
            "PN",
            "PR",
            "PS",
            "PT",
            "PU",
            "PW",
            "PY",
            "PZ",
            "QA",
            "QM",
            "QN",
            "QO",
            "QP",
            "QQ",
            "QR",
            "QS",
            "QT",
            "QU",
            "QV",
            "QW",
            "QX",
            "QY",
            "QZ",
            "RA",
            "RB",
            "RC",
            "RE",
            "RH",
            "RI",
            "RL",
            "RM",
            "RN",
            "RO",
            "RP",
            "RS",
            "RU",
            "RW",
            "SA",
            "SB",
            "SC",
            "SD",
            "SE",
            "SF",
            "SG",
            "SH",
            "SI",
            "SJ",
            "SK",
            "SL",
            "SM",
            "SN",
            "SO",
            "SR",
            "SS",
            "ST",
            "SU",
            "SV",
            "SX",
            "SY",
            "SZ",
            "TA",
            "TC",
            "TD",
            "TF",
            "TG",
            "TH",
            "TJ",
            "TK",
            "TL",
            "TM",
            "TN",
            "TO",
            "TP",
            "TR",
            "TT",
            "TV",
            "TW",
            "TZ",
            "UA",
            "UG",
            "UK",
            "UM",
            "UN",
            "US",
            "UY",
            "UZ",
            "VA",
            "VC",
            "VD",
            "VE",
            "VG",
            "VI",
            "VN",
            "VU",
            "WF",
            "WG",
            "WK",
            "WL",
            "WO",
            "WS",
            "WV",
            "XA",
            "XB",
            "XC",
            "XD",
            "XE",
            "XF",
            "XG",
            "XH",
            "XI",
            "XJ",
            "XK",
            "XL",
            "XM",
            "XN",
            "XO",
            "XP",
            "XQ",
            "XR",
            "XS",
            "XT",
            "XU",
            "XV",
            "XW",
            "XX",
            "XY",
            "XZ",
            "YD",
            "YE",
            "YT",
            "YU",
            "YV",
            "ZA",
            "ZM",
            "ZR",
            "ZW",
            "ZZ",
    };

    static {
        Arrays.sort(CCODES); // we cannot assume the codes are sorted
        Arrays.sort(SPECIALS); // Just in case ...
    }

    /**
     * Gets the singleton instance of the ISIN validator.
     *
     * @param checkCountryCode whether to check the country-code prefix or not
     * @return A singleton instance of the appropriate ISIN validator.
     */
    public static ISINValidator getInstance(final boolean checkCountryCode) {
        return checkCountryCode ? ISIN_VALIDATOR_TRUE : ISIN_VALIDATOR_FALSE;
    }

    private final boolean checkCountryCode;

    private ISINValidator(final boolean checkCountryCode) {
        this.checkCountryCode = checkCountryCode;
    }

    private boolean checkCode(final String code) {
        return Arrays.binarySearch(CCODES, code) >= 0
               ||
               Arrays.binarySearch(SPECIALS, code) >= 0
        ;
    }

    /**
     * Tests whether the code is a valid ISIN code after any transformation
     * by the validate routine.
     *
     * @param code The code to validate.
     * @return {@code true} if a valid ISIN
     * code, otherwise {@code false}.
     */
    public boolean isValid(final String code) {
        final boolean valid = VALIDATOR.isValid(code);
        if (valid && checkCountryCode) {
            return checkCode(code.substring(0, 2));
        }
        return valid;
    }

    /**
     * Checks the code is valid ISIN code.
     *
     * @param code The code to validate.
     * @return A valid ISIN code if valid, otherwise {@code null}.
     */
    public Object validate(final String code) {
        final Object validate = VALIDATOR.validate(code);
        if (validate != null && checkCountryCode) {
            return checkCode(code.substring(0, 2)) ? validate : null;
        }
        return validate;
    }

}