    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.ISSNCheckDigit;
    024
    025/**
    026 * International Standard Serial Number (ISSN)
    027 * is an eight-digit serial number used to
    028 * uniquely identify a serial publication.
    029 * <pre>
    030 * The format is:
    031 *
    032 * ISSN dddd-dddC
    033 * where:
    034 * d = decimal digit (0-9)
    035 * C = checksum (0-9 or X)
    036 *
    037 * The checksum is formed by adding the first 7 digits multiplied by
    038 * the position in the entire number (counting from the right).
    039 *
    040 * For example, abcd-efg would be 8a + 7b + 6c + 5d + 4e +3f +2g.
    041 * The check digit is modulus 11, where the value 10 is represented by 'X'
    042 * For example:
    043 * ISSN 0317-8471
    044 * ISSN 1050-124X
    045 *
    046 * This class strips off the 'ISSN ' prefix if it is present before passing
    047 * the remainder to the checksum routine.
    048 *
    049 * </pre>
    050 * <p>
    051 * Note: the {@link #isValid(String)} and {@link #validate(String)} methods strip off any leading
    052 * or trailing spaces before doing the validation.
    053 * To ensure that only a valid code (without 'ISSN ' prefix) is passed to a method,
    054 * use the following code:
    055 * <pre>
    056 * Object valid = validator.validate(input);
    057 * if (valid != null) {
    058 *    some_method(valid.toString());
    059 * }
    060 * </pre>
    061 * @since 1.5.0
    062 */
    063public class ISSNValidator implements Serializable {
    064
    065    private static final long serialVersionUID = 4319515687976420405L;
    066
    067    private static final String ISSN_REGEX = "(?:ISSN )?(\\d{4})-(\\d{3}[0-9X])$"; // We don't include the '-' in the code, so it is 8 chars
    068
    069    private static final int ISSN_LEN = 8;
    070
    071    private static final String ISSN_PREFIX = "977";
    072
    073    private static final String EAN_ISSN_REGEX = "^(977)(?:(\\d{10}))$";
    074
    075    private static final int EAN_ISSN_LEN = 13;
    076
    077    private static final CodeValidator VALIDATOR = new CodeValidator(ISSN_REGEX, ISSN_LEN, ISSNCheckDigit.ISSN_CHECK_DIGIT);
    078
    079    private static final CodeValidator EAN_VALIDATOR = new CodeValidator(EAN_ISSN_REGEX, EAN_ISSN_LEN, EAN13CheckDigit.EAN13_CHECK_DIGIT);
    080
    081    /** ISSN Code Validator */
    082    private static final ISSNValidator ISSN_VALIDATOR = new ISSNValidator();
    083
    084    /**
    085     * Return a singleton instance of the ISSN validator
    086     *
    087     * @return A singleton instance of the ISSN validator.
    088     */
    089    public static ISSNValidator getInstance() {
    090        return ISSN_VALIDATOR;
    091    }
    092
    093    /**
    094     * Convert an ISSN code to an EAN-13 code.
    095     * <p>
    096     * This method requires a valid ISSN code.
    097     * It may contain a leading 'ISSN ' prefix,
    098     * as the input is passed through the {@link #validate(String)}
    099     * method.
    100     *
    101     * @param issn The ISSN code to convert
    102     * @param suffix the two digit suffix, e.g. "00"
    103     * @return A converted EAN-13 code or <code>null</code>
    104     * if the input ISSN code is not valid
    105     */
    106    public String convertToEAN13(final String issn, final String suffix) {
    107
    108        if (suffix == null || !suffix.matches("\\d\\d")) {
    109            throw new IllegalArgumentException("Suffix must be two digits: '" + suffix + "'");
    110        }
    111
    112        final Object result = validate(issn);
    113        if (result == null) {
    114            return null;
    115        }
    116
    117        // Calculate the new EAN-13 code
    118        final String input = result.toString();
    119        String ean13 = ISSN_PREFIX + input.substring(0, input.length() - 1) + suffix;
    120        try {
    121            final String checkDigit = EAN13CheckDigit.EAN13_CHECK_DIGIT.calculate(ean13);
    122            ean13 += checkDigit;
    123            return ean13;
    124        } catch (final CheckDigitException e) { // Should not happen
    125            throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage());
    126        }
    127
    128    }
    129
    130    /**
    131     * Extract an ISSN code from an ISSN-EAN-13 code.
    132     * <p>
    133     * This method requires a valid ISSN-EAN-13 code with NO formatting
    134     * characters.
    135     * That is a 13 digit EAN-13 code with the '977' prefix
    136     *
    137     * @param ean13 The ISSN code to convert
    138     * @return A valid ISSN code or <code>null</code>
    139     * if the input ISSN EAN-13 code is not valid
    140     * @since 1.7
    141     */
    142    public String extractFromEAN13(final String ean13) {
    143        String input = ean13.trim();
    144        if (input.length() != EAN_ISSN_LEN ) {
    145            throw new IllegalArgumentException("Invalid length " + input.length() + " for '" + input + "'");
    146        }
    147        if (!input.startsWith(ISSN_PREFIX)) {
    148            throw new IllegalArgumentException("Prefix must be " + ISSN_PREFIX + " to contain an ISSN: '" + ean13 + "'");
    149        }
    150        final Object result = validateEan(input);
    151        if (result == null) {
    152            return null;
    153        }
    154        // Calculate the ISSN code
    155        input = result.toString();
    156        try {
    157            //CHECKSTYLE:OFF: MagicNumber
    158            final String issnBase = input.substring(3, 10); // TODO: how to derive these
    159            //CHECKSTYLE:ON: MagicNumber
    160            final String checkDigit = ISSNCheckDigit.ISSN_CHECK_DIGIT.calculate(issnBase);
    161            final String issn = issnBase + checkDigit;
    162            return issn;
    163        } catch (final CheckDigitException e) { // Should not happen
    164            throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage());
    165        }
    166    }
    167
    168    /**
    169     * Check the code is a valid ISSN code after any transformation
    170     * by the validate routine.
    171     * @param code The code to validate.
    172     * @return {@code true} if a valid ISSN
    173     * code, otherwise {@code false}.
    174     */
    175    public boolean isValid(final String code) {
    176        return VALIDATOR.isValid(code);
    177    }
    178
    179    /**
    180     * Check the code is valid ISSN code.
    181     * <p>
    182     * If valid, this method returns the ISSN code with
    183     * the 'ISSN ' prefix removed (if it was present)
    184     *
    185     * @param code The code to validate.
    186     * @return A valid ISSN code if valid, otherwise <code>null</code>.
    187     */
    188    public Object validate(final String code) {
    189        return VALIDATOR.validate(code);
    190    }
    191
    192    /**
    193     * Check the code is a valid EAN code.
    194     * <p>
    195     * If valid, this method returns the EAN code
    196     *
    197     * @param code The code to validate.
    198     * @return A valid EAN code if valid, otherwise <code>null</code>.
    199     * @since 1.7
    200     */
    201    public Object validateEan(final String code) {
    202        return EAN_VALIDATOR.validate(code);
    203    }
    204}