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