1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * https://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.validator.routines; 18 19 import java.io.Serializable; 20 21 import org.apache.commons.validator.routines.checkdigit.CheckDigitException; 22 import org.apache.commons.validator.routines.checkdigit.EAN13CheckDigit; 23 import org.apache.commons.validator.routines.checkdigit.ISSNCheckDigit; 24 25 /** 26 * International Standard Serial Number (ISSN) 27 * is an eight-digit serial number used to 28 * uniquely identify a serial publication. 29 * <pre> 30 * The format is: 31 * 32 * ISSN dddd-dddC 33 * where: 34 * d = decimal digit (0-9) 35 * C = checksum (0-9 or X) 36 * 37 * The checksum is formed by adding the first 7 digits multiplied by 38 * the position in the entire number (counting from the right). 39 * 40 * For example, abcd-efg would be 8a + 7b + 6c + 5d + 4e +3f +2g. 41 * The check digit is modulus 11, where the value 10 is represented by 'X' 42 * For example: 43 * ISSN 0317-8471 44 * ISSN 1050-124X 45 * 46 * This class strips off the 'ISSN ' prefix if it is present before passing 47 * the remainder to the checksum routine. 48 * 49 * </pre> 50 * <p> 51 * Note: the {@link #isValid(String)} and {@link #validate(String)} methods strip off any leading 52 * or trailing spaces before doing the validation. 53 * To ensure that only a valid code (without 'ISSN ' prefix) is passed to a method, 54 * use the following code: 55 * </p> 56 * <pre> 57 * Object valid = validator.validate(input); 58 * if (valid != null) { 59 * some_method(valid.toString()); 60 * } 61 * </pre> 62 * @since 1.5.0 63 */ 64 public class ISSNValidator implements Serializable { 65 66 private static final long serialVersionUID = 4319515687976420405L; 67 68 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 69 70 private static final int ISSN_LEN = 8; 71 72 private static final String ISSN_PREFIX = "977"; 73 74 private static final String EAN_ISSN_REGEX = "^(977)(?:(\\d{10}))$"; 75 76 private static final int EAN_ISSN_LEN = 13; 77 78 private static final CodeValidator VALIDATOR = new CodeValidator(ISSN_REGEX, ISSN_LEN, ISSNCheckDigit.ISSN_CHECK_DIGIT); 79 80 private static final CodeValidator EAN_VALIDATOR = new CodeValidator(EAN_ISSN_REGEX, EAN_ISSN_LEN, EAN13CheckDigit.EAN13_CHECK_DIGIT); 81 82 /** ISSN Code Validator. */ 83 private static final ISSNValidator ISSN_VALIDATOR = new ISSNValidator(); 84 85 /** 86 * Gets the singleton instance of the ISSN validator. 87 * 88 * @return A singleton instance of the ISSN validator. 89 */ 90 public static ISSNValidator getInstance() { 91 return ISSN_VALIDATOR; 92 } 93 94 /** 95 * Constructs a new instance. 96 */ 97 public ISSNValidator() { 98 // empty 99 } 100 101 /** 102 * Converts an ISSN code to an EAN-13 code. 103 * <p> 104 * This method requires a valid ISSN code. 105 * It may contain a leading 'ISSN ' prefix, 106 * as the input is passed through the {@link #validate(String)} 107 * method. 108 * </p> 109 * 110 * @param issn The ISSN code to convert 111 * @param suffix the two digit suffix, for example, "00" 112 * @return A converted EAN-13 code or {@code null} 113 * if the input ISSN code is not valid 114 */ 115 public String convertToEAN13(final String issn, final String suffix) { 116 if (suffix == null || !suffix.matches("\\d\\d")) { 117 throw new IllegalArgumentException("Suffix must be two digits: '" + suffix + "'"); 118 } 119 final Object result = validate(issn); 120 if (result == null) { 121 return null; 122 } 123 // Calculate the new EAN-13 code 124 final String input = result.toString(); 125 String ean13 = ISSN_PREFIX + input.substring(0, input.length() - 1) + suffix; 126 try { 127 final String checkDigit = EAN13CheckDigit.EAN13_CHECK_DIGIT.calculate(ean13); 128 ean13 += checkDigit; 129 return ean13; 130 } catch (final CheckDigitException e) { // Should not happen 131 throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage()); 132 } 133 } 134 135 /** 136 * Extracts an ISSN code from an ISSN-EAN-13 code. 137 * <p> 138 * This method requires a valid ISSN-EAN-13 code with NO formatting 139 * characters. 140 * That is a 13 digit EAN-13 code with the '977' prefix. 141 * </p> 142 * 143 * @param ean13 The ISSN code to convert 144 * @return A valid ISSN code or {@code null} 145 * if the input ISSN EAN-13 code is not valid 146 * @since 1.7 147 */ 148 public String extractFromEAN13(final String ean13) { 149 String input = ean13.trim(); 150 if (input.length() != EAN_ISSN_LEN) { 151 throw new IllegalArgumentException("Invalid length " + input.length() + " for '" + input + "'"); 152 } 153 if (!input.startsWith(ISSN_PREFIX)) { 154 throw new IllegalArgumentException("Prefix must be " + ISSN_PREFIX + " to contain an ISSN: '" + ean13 + "'"); 155 } 156 final Object result = validateEan(input); 157 if (result == null) { 158 return null; 159 } 160 // Calculate the ISSN code 161 input = result.toString(); 162 try { 163 //CHECKSTYLE:OFF: MagicNumber 164 final String issnBase = input.substring(3, 10); // TODO: how to derive these 165 //CHECKSTYLE:ON: MagicNumber 166 final String checkDigit = ISSNCheckDigit.ISSN_CHECK_DIGIT.calculate(issnBase); 167 return issnBase + checkDigit; 168 } catch (final CheckDigitException e) { // Should not happen 169 throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage()); 170 } 171 } 172 173 /** 174 * Tests whether the code is a valid ISSN code after any transformation 175 * by the validate routine. 176 * 177 * @param code The code to validate. 178 * @return {@code true} if a valid ISSN 179 * code, otherwise {@code false}. 180 */ 181 public boolean isValid(final String code) { 182 return VALIDATOR.isValid(code); 183 } 184 185 /** 186 * Checks the code is valid ISSN code. 187 * <p> 188 * If valid, this method returns the ISSN code with 189 * the 'ISSN ' prefix removed (if it was present) 190 * </p> 191 * 192 * @param code The code to validate. 193 * @return A valid ISSN code if valid, otherwise {@code null}. 194 */ 195 public Object validate(final String code) { 196 return VALIDATOR.validate(code); 197 } 198 199 /** 200 * Checks the code is a valid EAN code. 201 * <p> 202 * If valid, this method returns the EAN code 203 * </p> 204 * 205 * @param code The code to validate. 206 * @return A valid EAN code if valid, otherwise {@code null}. 207 * @since 1.7 208 */ 209 public Object validateEan(final String code) { 210 return EAN_VALIDATOR.validate(code); 211 } 212 }