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 *
63 * @since 1.5.0
64 */
65 public class ISSNValidator implements Serializable {
66
67 private static final long serialVersionUID = 4319515687976420405L;
68
69 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
70
71 private static final int ISSN_LEN = 8;
72
73 private static final String ISSN_PREFIX = "977";
74
75 private static final String EAN_ISSN_REGEX = "^(977)(?:(\\d{10}))$";
76
77 private static final int EAN_ISSN_LEN = 13;
78
79 private static final CodeValidator VALIDATOR = new CodeValidator(ISSN_REGEX, ISSN_LEN, ISSNCheckDigit.ISSN_CHECK_DIGIT);
80
81 private static final CodeValidator EAN_VALIDATOR = new CodeValidator(EAN_ISSN_REGEX, EAN_ISSN_LEN, EAN13CheckDigit.EAN13_CHECK_DIGIT);
82
83 /** ISSN Code Validator. */
84 private static final ISSNValidator ISSN_VALIDATOR = new ISSNValidator();
85
86 /**
87 * Gets the singleton instance of the ISSN validator.
88 *
89 * @return A singleton instance of the ISSN validator.
90 */
91 public static ISSNValidator getInstance() {
92 return ISSN_VALIDATOR;
93 }
94
95 /**
96 * Constructs a new instance.
97 */
98 public ISSNValidator() {
99 // 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 }