View Javadoc
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 }