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; 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 * </p> 056 * <pre> 057 * Object valid = validator.validate(input); 058 * if (valid != null) { 059 * some_method(valid.toString()); 060 * } 061 * </pre> 062 * 063 * @since 1.5.0 064 */ 065public class ISSNValidator implements Serializable { 066 067 private static final long serialVersionUID = 4319515687976420405L; 068 069 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 070 071 private static final int ISSN_LEN = 8; 072 073 private static final String ISSN_PREFIX = "977"; 074 075 private static final String EAN_ISSN_REGEX = "^(977)(?:(\\d{10}))$"; 076 077 private static final int EAN_ISSN_LEN = 13; 078 079 private static final CodeValidator VALIDATOR = new CodeValidator(ISSN_REGEX, ISSN_LEN, ISSNCheckDigit.ISSN_CHECK_DIGIT); 080 081 private static final CodeValidator EAN_VALIDATOR = new CodeValidator(EAN_ISSN_REGEX, EAN_ISSN_LEN, EAN13CheckDigit.EAN13_CHECK_DIGIT); 082 083 /** ISSN Code Validator. */ 084 private static final ISSNValidator ISSN_VALIDATOR = new ISSNValidator(); 085 086 /** 087 * Gets the singleton instance of the ISSN validator. 088 * 089 * @return A singleton instance of the ISSN validator. 090 */ 091 public static ISSNValidator getInstance() { 092 return ISSN_VALIDATOR; 093 } 094 095 /** 096 * Constructs a new instance. 097 */ 098 public ISSNValidator() { 099 // empty 100 } 101 102 /** 103 * Converts an ISSN code to an EAN-13 code. 104 * <p> 105 * This method requires a valid ISSN code. 106 * It may contain a leading 'ISSN ' prefix, 107 * as the input is passed through the {@link #validate(String)} 108 * method. 109 * </p> 110 * 111 * @param issn The ISSN code to convert 112 * @param suffix the two digit suffix, for example, "00" 113 * @return A converted EAN-13 code or {@code null} 114 * if the input ISSN code is not valid 115 */ 116 public String convertToEAN13(final String issn, final String suffix) { 117 if (suffix == null || !suffix.matches("\\d\\d")) { 118 throw new IllegalArgumentException("Suffix must be two digits: '" + suffix + "'"); 119 } 120 final Object result = validate(issn); 121 if (result == null) { 122 return null; 123 } 124 // Calculate the new EAN-13 code 125 final String input = result.toString(); 126 String ean13 = ISSN_PREFIX + input.substring(0, input.length() - 1) + suffix; 127 try { 128 final String checkDigit = EAN13CheckDigit.EAN13_CHECK_DIGIT.calculate(ean13); 129 ean13 += checkDigit; 130 return ean13; 131 } catch (final CheckDigitException e) { // Should not happen 132 throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage()); 133 } 134 } 135 136 /** 137 * Extracts an ISSN code from an ISSN-EAN-13 code. 138 * <p> 139 * This method requires a valid ISSN-EAN-13 code with NO formatting 140 * characters. 141 * That is a 13 digit EAN-13 code with the '977' prefix. 142 * </p> 143 * 144 * @param ean13 The ISSN code to convert 145 * @return A valid ISSN code or {@code null} 146 * if the input ISSN EAN-13 code is not valid 147 * @since 1.7 148 */ 149 public String extractFromEAN13(final String ean13) { 150 String input = ean13.trim(); 151 if (input.length() != EAN_ISSN_LEN) { 152 throw new IllegalArgumentException("Invalid length " + input.length() + " for '" + input + "'"); 153 } 154 if (!input.startsWith(ISSN_PREFIX)) { 155 throw new IllegalArgumentException("Prefix must be " + ISSN_PREFIX + " to contain an ISSN: '" + ean13 + "'"); 156 } 157 final Object result = validateEan(input); 158 if (result == null) { 159 return null; 160 } 161 // Calculate the ISSN code 162 input = result.toString(); 163 try { 164 //CHECKSTYLE:OFF: MagicNumber 165 final String issnBase = input.substring(3, 10); // TODO: how to derive these 166 //CHECKSTYLE:ON: MagicNumber 167 final String checkDigit = ISSNCheckDigit.ISSN_CHECK_DIGIT.calculate(issnBase); 168 return issnBase + checkDigit; 169 } catch (final CheckDigitException e) { // Should not happen 170 throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage()); 171 } 172 } 173 174 /** 175 * Tests whether the code is a valid ISSN code after any transformation 176 * by the validate routine. 177 * 178 * @param code The code to validate. 179 * @return {@code true} if a valid ISSN 180 * code, otherwise {@code false}. 181 */ 182 public boolean isValid(final String code) { 183 return VALIDATOR.isValid(code); 184 } 185 186 /** 187 * Checks the code is valid ISSN code. 188 * <p> 189 * If valid, this method returns the ISSN code with 190 * the 'ISSN ' prefix removed (if it was present) 191 * </p> 192 * 193 * @param code The code to validate. 194 * @return A valid ISSN code if valid, otherwise {@code null}. 195 */ 196 public Object validate(final String code) { 197 return VALIDATOR.validate(code); 198 } 199 200 /** 201 * Checks the code is a valid EAN code. 202 * <p> 203 * If valid, this method returns the EAN code 204 * </p> 205 * 206 * @param code The code to validate. 207 * @return A valid EAN code if valid, otherwise {@code null}. 208 * @since 1.7 209 */ 210 public Object validateEan(final String code) { 211 return EAN_VALIDATOR.validate(code); 212 } 213}