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