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}