    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.validator.routines;
    018
    019import java.io.Serializable;
    020import java.util.Objects;
    021
    022import org.apache.commons.validator.routines.checkdigit.CheckDigitException;
    023import org.apache.commons.validator.routines.checkdigit.EAN13CheckDigit;
    024import org.apache.commons.validator.routines.checkdigit.ISBN10CheckDigit;
    025
    026/**
    027 * <strong>ISBN-10</strong> and <strong>ISBN-13</strong> Code Validation.
    028 * <p>
    029 * This validator validates the code is either a valid ISBN-10
    030 * (using a {@link CodeValidator} with the {@link ISBN10CheckDigit})
    031 * or a valid ISBN-13 code (using a {@link CodeValidator} with
    032 * the {@link EAN13CheckDigit} routine).
    033 * </p>
    034 * <p>
    035 * The {@code validate()} methods return the ISBN code with formatting
    036 * characters removed if valid or {@code null} if invalid.
    037 * </p>
    038 * <p>
    039 * This validator also provides the facility to convert ISBN-10 codes to
    040 * ISBN-13 if the {@code convert} property is {@code true}.
    041 * </p>
    042 * <p>
    043 * From 1st January 2007 the book industry will start to use a new 13 digit
    044 * ISBN number (rather than this 10 digit ISBN number). ISBN-13 codes are
    045 * <a href="https://en.wikipedia.org/wiki/European_Article_Number">EAN</a>
    046 * codes, for more information see:
    047 * </p>
    048 *
    049 * <ul>
    050 *   <li><a href="https://en.wikipedia.org/wiki/ISBN">Wikipedia - International
    051 *       Standard Book Number (ISBN)</a>.</li>
    052 *   <li>EAN - see
    053 *       <a href="https://en.wikipedia.org/wiki/European_Article_Number">Wikipedia -
    054 *       European Article Number</a>.</li>
    055 *   <li><a href="https://www.isbn.org/standards/home/isbn/transition.asp">ISBN-13
    056 *       Transition details</a>.</li>
    057 * </ul>
    058 *
    059 * <p>
    060 * ISBN-13s are either prefixed with 978 or 979. 978 prefixes are only assigned
    061 * to the ISBN agency. 979 prefixes may be assigned to ISBNs or ISMNs
    062 * (<a href="https://www.ismn-international.org/">International
    063 * Standard Music Numbers</a>).
    064 * </p>
    065 * <ul>
    066 *     <li>979-0 are assigned to the ISMN agency</li>
    067 *     <li>979-10, 979-11, 979-12 are assigned to the ISBN agency</li>
    068 * </ul>
    069 * <p>
    070 * All other 979 prefixed EAN-13 numbers have not yet been assigned to an agency. The
    071 * validator validates all 13-digit codes with 978 or 979 prefixes.
    072 * </p>
    073 *
    074 * @since 1.4
    075 */
    076public class ISBNValidator implements Serializable {
    077
    078    private static final int ISBN_10_LEN = 10;
    079
    080    private static final long serialVersionUID = 4319515687976420405L;
    081
    082    private static final String SEP = "(?:\\-|\\s)";
    083    private static final String GROUP = "(\\d{1,5})";
    084    private static final String PUBLISHER = "(\\d{1,7})";
    085    private static final String TITLE = "(\\d{1,6})";
    086
    087    /**
    088     * ISBN-10 consists of 4 groups of numbers separated by either dashes (-)
    089     * or spaces.  The first group is 1-5 characters, second 1-7, third 1-6,
    090     * and fourth is 1 digit or an X.
    091     */
    092    static final String ISBN10_REGEX = "^(?:(\\d{9}[0-9X])|(?:" + GROUP + SEP + PUBLISHER + SEP + TITLE + SEP + "([0-9X])))$";
    093
    094    /**
    095     * ISBN-13 consists of 5 groups of numbers separated by either dashes (-)
    096     * or spaces.  The first group is 978 or 979, the second group is
    097     * 1-5 characters, third 1-7, fourth 1-6, and fifth is 1 digit.
    098     */
    099    static final String ISBN13_REGEX = "^(978|979)(?:(\\d{10})|(?:" + SEP + GROUP + SEP + PUBLISHER + SEP + TITLE + SEP + "([0-9])))$";
    100
    101    /** ISBN Code Validator (which converts ISBN-10 codes to ISBN-13 */
    102    private static final ISBNValidator ISBN_VALIDATOR = new ISBNValidator();
    103
    104    /** ISBN Code Validator (which converts ISBN-10 codes to ISBN-13 */
    105    private static final ISBNValidator ISBN_VALIDATOR_NO_CONVERT = new ISBNValidator(false);
    106
    107    /**
    108     * Gets the singleton instance of the ISBN validator which
    109     * converts ISBN-10 codes to ISBN-13.
    110     *
    111     * @return A singleton instance of the ISBN validator.
    112     */
    113    public static ISBNValidator getInstance() {
    114        return ISBN_VALIDATOR;
    115    }
    116
    117    /**
    118     * Gets the singleton instance of the ISBN validator specifying
    119     * whether ISBN-10 codes should be converted to ISBN-13.
    120     *
    121     * @param convert {@code true} if valid ISBN-10 codes
    122     * should be converted to ISBN-13 codes or {@code false}
    123     * if valid ISBN-10 codes should be returned unchanged.
    124     * @return A singleton instance of the ISBN validator.
    125     */
    126    public static ISBNValidator getInstance(final boolean convert) {
    127        return convert ? ISBN_VALIDATOR : ISBN_VALIDATOR_NO_CONVERT;
    128    }
    129
    130    /** ISBN-10 Code Validator */
    131    private final CodeValidator isbn10Validator = new CodeValidator(ISBN10_REGEX, 10, ISBN10CheckDigit.ISBN10_CHECK_DIGIT);
    132
    133    /** ISBN-13 Code Validator */
    134    private final CodeValidator isbn13Validator = new CodeValidator(ISBN13_REGEX, 13, EAN13CheckDigit.EAN13_CHECK_DIGIT);
    135
    136    /**
    137     * Whether validation converts an ISBN-10 codes to ISBN-13.
    138     */
    139    private final boolean convert;
    140
    141    /**
    142     * Constructs an ISBN validator which converts ISBN-10 codes
    143     * to ISBN-13.
    144     */
    145    public ISBNValidator() {
    146        this(true);
    147    }
    148
    149    /**
    150     * Constructs an ISBN validator indicating whether
    151     * ISBN-10 codes should be converted to ISBN-13.
    152     *
    153     * @param convert {@code true} if valid ISBN-10 codes
    154     * should be converted to ISBN-13 codes or {@code false}
    155     * if valid ISBN-10 codes should be returned unchanged.
    156     */
    157    public ISBNValidator(final boolean convert) {
    158        this.convert = convert;
    159    }
    160
    161    /**
    162     * Convert an ISBN-10 code to an ISBN-13 code.
    163     * <p>
    164     * This method requires a valid ISBN-10 with NO formatting
    165     * characters.
    166     * </p>
    167     *
    168     * @param isbn10 The ISBN-10 code to convert
    169     * @return A converted ISBN-13 code or {@code null}
    170     * if the ISBN-10 code is not valid
    171     */
    172    public String convertToISBN13(final String isbn10) {
    173
    174        if (isbn10 == null) {
    175            return null;
    176        }
    177
    178        final String input = isbn10.trim();
    179        if (input.length() != ISBN_10_LEN) {
    180            throw new IllegalArgumentException("Invalid length " + input.length() + " for '" + input + "'");
    181        }
    182
    183        // Calculate the new ISBN-13 code (drop the original checkdigit)
    184        String isbn13 = "978" + input.substring(0, ISBN_10_LEN - 1);
    185        try {
    186            final String checkDigit = isbn13Validator.getCheckDigit().calculate(isbn13);
    187            isbn13 += checkDigit;
    188            return isbn13;
    189        } catch (final CheckDigitException e) {
    190            throw new IllegalArgumentException("Check digit error for '" + input + "' - " + e.getMessage());
    191        }
    192
    193    }
    194
    195    /**
    196     * Check the code is either a valid ISBN-10 or ISBN-13 code.
    197     *
    198     * @param code The code to validate.
    199     * @return {@code true} if a valid ISBN-10 or
    200     * ISBN-13 code, otherwise {@code false}.
    201     */
    202    public boolean isValid(final String code) {
    203        return isValidISBN13(code) || isValidISBN10(code);
    204    }
    205
    206    /**
    207     * Check the code is a valid ISBN-10 code.
    208     *
    209     * @param code The code to validate.
    210     * @return {@code true} if a valid ISBN-10
    211     * code, otherwise {@code false}.
    212     */
    213    public boolean isValidISBN10(final String code) {
    214        return isbn10Validator.isValid(code);
    215    }
    216
    217    /**
    218     * Check the code is a valid ISBN-13 code.
    219     *
    220     * @param code The code to validate.
    221     * @return {@code true} if a valid ISBN-13
    222     * code, otherwise {@code false}.
    223     */
    224    public boolean isValidISBN13(final String code) {
    225        return isbn13Validator.isValid(code);
    226    }
    227
    228    /**
    229     * Check the code is either a valid ISBN-10 or ISBN-13 code.
    230     * <p>
    231     * If valid, this method returns the ISBN code with
    232     * formatting characters removed (such as space or hyphen).
    233     * </p>
    234     * <p>
    235     * Converts an ISBN-10 codes to ISBN-13 if
    236     * {@code convertToISBN13} is {@code true}.
    237     * </p>
    238     *
    239     * @param code The code to validate.
    240     * @return A valid ISBN code if valid, otherwise {@code null}.
    241     */
    242    public String validate(final String code) {
    243        String result = validateISBN13(code);
    244        if (result == null) {
    245            result = validateISBN10(code);
    246            if (result != null && convert) {
    247                result = convertToISBN13(result);
    248            }
    249        }
    250        return result;
    251    }
    252
    253    /**
    254     * Check the code is a valid ISBN-10 code.
    255     * <p>
    256     * If valid, this method returns the ISBN-10 code with
    257     * formatting characters removed (such as space or hyphen).
    258     * </p>
    259     *
    260     * @param code The code to validate.
    261     * @return A valid ISBN-10 code if valid,
    262     * otherwise {@code null}.
    263     */
    264    public String validateISBN10(final String code) {
    265        return Objects.toString(isbn10Validator.validate(code), null);
    266    }
    267
    268    /**
    269     * Check the code is a valid ISBN-13 code.
    270     * <p>
    271     * If valid, this method returns the ISBN-13 code with
    272     * formatting characters removed (such as space or hyphen).
    273     * </p>
    274     *
    275     * @param code The code to validate.
    276     * @return A valid ISBN-13 code if valid,
    277     * otherwise {@code null}.
    278     */
    279    public String validateISBN13(final String code) {
    280        return Objects.toString(isbn13Validator.validate(code), null);
    281    }
    282
    283}