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    *      http://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       * Converts an ISSN code to an EAN-13 code.
96       * <p>
97       * This method requires a valid ISSN code.
98       * It may contain a leading 'ISSN ' prefix,
99       * as the input is passed through the {@link #validate(String)}
100      * method.
101      * </p>
102      *
103      * @param issn The ISSN code to convert
104      * @param suffix the two digit suffix, e.g. "00"
105      * @return A converted EAN-13 code or {@code null}
106      * if the input ISSN code is not valid
107      */
108     public String convertToEAN13(final String issn, final String suffix) {
109         if (suffix == null || !suffix.matches("\\d\\d")) {
110             throw new IllegalArgumentException("Suffix must be two digits: '" + suffix + "'");
111         }
112         final Object result = validate(issn);
113         if (result == null) {
114             return null;
115         }
116         // Calculate the new EAN-13 code
117         final String input = result.toString();
118         String ean13 = ISSN_PREFIX + input.substring(0, input.length() - 1) + suffix;
119         try {
120             final String checkDigit = EAN13CheckDigit.EAN13_CHECK_DIGIT.calculate(ean13);
121             ean13 += checkDigit;
122             return ean13;
123         } catch (final CheckDigitException e) { // Should not happen
124             throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage());
125         }
126     }
127 
128     /**
129      * Extracts an ISSN code from an ISSN-EAN-13 code.
130      * <p>
131      * This method requires a valid ISSN-EAN-13 code with NO formatting
132      * characters.
133      * That is a 13 digit EAN-13 code with the '977' prefix.
134      * </p>
135      *
136      * @param ean13 The ISSN code to convert
137      * @return A valid ISSN code or {@code null}
138      * if the input ISSN EAN-13 code is not valid
139      * @since 1.7
140      */
141     public String extractFromEAN13(final String ean13) {
142         String input = ean13.trim();
143         if (input.length() != EAN_ISSN_LEN ) {
144             throw new IllegalArgumentException("Invalid length " + input.length() + " for '" + input + "'");
145         }
146         if (!input.startsWith(ISSN_PREFIX)) {
147             throw new IllegalArgumentException("Prefix must be " + ISSN_PREFIX + " to contain an ISSN: '" + ean13 + "'");
148         }
149         final Object result = validateEan(input);
150         if (result == null) {
151             return null;
152         }
153         // Calculate the ISSN code
154         input = result.toString();
155         try {
156             //CHECKSTYLE:OFF: MagicNumber
157             final String issnBase = input.substring(3, 10); // TODO: how to derive these
158             //CHECKSTYLE:ON: MagicNumber
159             final String checkDigit = ISSNCheckDigit.ISSN_CHECK_DIGIT.calculate(issnBase);
160             return issnBase + checkDigit;
161         } catch (final CheckDigitException e) { // Should not happen
162             throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage());
163         }
164     }
165 
166     /**
167      * Tests whether the code is a valid ISSN code after any transformation
168      * by the validate routine.
169      *
170      * @param code The code to validate.
171      * @return {@code true} if a valid ISSN
172      * code, otherwise {@code false}.
173      */
174     public boolean isValid(final String code) {
175         return VALIDATOR.isValid(code);
176     }
177 
178     /**
179      * Checks the code is valid ISSN code.
180      * <p>
181      * If valid, this method returns the ISSN code with
182      * the 'ISSN ' prefix removed (if it was present)
183      * </p>
184      *
185      * @param code The code to validate.
186      * @return A valid ISSN code if valid, otherwise {@code null}.
187      */
188     public Object validate(final String code) {
189         return VALIDATOR.validate(code);
190     }
191 
192     /**
193      * Checks the code is a valid EAN code.
194      * <p>
195      * If valid, this method returns the EAN code
196      * </p>
197      *
198      * @param code The code to validate.
199      * @return A valid EAN code if valid, otherwise {@code null}.
200      * @since 1.7
201      */
202     public Object validateEan(final String code) {
203         return EAN_VALIDATOR.validate(code);
204     }
205 }