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 }