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   * <pre>
56   * Object valid = validator.validate(input);
57   * if (valid != null) {
58   *    some_method(valid.toString());
59   * }
60   * </pre>
61   * @since 1.5.0
62   */
63  public class ISSNValidator implements Serializable {
64  
65      private static final long serialVersionUID = 4319515687976420405L;
66  
67      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
68  
69      private static final int ISSN_LEN = 8;
70  
71      private static final String ISSN_PREFIX = "977";
72  
73      private static final String EAN_ISSN_REGEX = "^(977)(?:(\\d{10}))$";
74  
75      private static final int EAN_ISSN_LEN = 13;
76  
77      private static final CodeValidator VALIDATOR = new CodeValidator(ISSN_REGEX, ISSN_LEN, ISSNCheckDigit.ISSN_CHECK_DIGIT);
78  
79      private static final CodeValidator EAN_VALIDATOR = new CodeValidator(EAN_ISSN_REGEX, EAN_ISSN_LEN, EAN13CheckDigit.EAN13_CHECK_DIGIT);
80  
81      /** ISSN Code Validator */
82      private static final ISSNValidator ISSN_VALIDATOR = new ISSNValidator();
83  
84      /**
85       * Return a singleton instance of the ISSN validator
86       *
87       * @return A singleton instance of the ISSN validator.
88       */
89      public static ISSNValidator getInstance() {
90          return ISSN_VALIDATOR;
91      }
92  
93      /**
94       * Convert an ISSN code to an EAN-13 code.
95       * <p>
96       * This method requires a valid ISSN code.
97       * It may contain a leading 'ISSN ' prefix,
98       * as the input is passed through the {@link #validate(String)}
99       * method.
100      *
101      * @param issn The ISSN code to convert
102      * @param suffix the two digit suffix, e.g. "00"
103      * @return A converted EAN-13 code or <code>null</code>
104      * if the input ISSN code is not valid
105      */
106     public String convertToEAN13(final String issn, final String suffix) {
107 
108         if (suffix == null || !suffix.matches("\\d\\d")) {
109             throw new IllegalArgumentException("Suffix must be two digits: '" + suffix + "'");
110         }
111 
112         final Object result = validate(issn);
113         if (result == null) {
114             return null;
115         }
116 
117         // Calculate the new EAN-13 code
118         final String input = result.toString();
119         String ean13 = ISSN_PREFIX + input.substring(0, input.length() - 1) + suffix;
120         try {
121             final String checkDigit = EAN13CheckDigit.EAN13_CHECK_DIGIT.calculate(ean13);
122             ean13 += checkDigit;
123             return ean13;
124         } catch (final CheckDigitException e) { // Should not happen
125             throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage());
126         }
127 
128     }
129 
130     /**
131      * Extract an ISSN code from an ISSN-EAN-13 code.
132      * <p>
133      * This method requires a valid ISSN-EAN-13 code with NO formatting
134      * characters.
135      * That is a 13 digit EAN-13 code with the '977' prefix
136      *
137      * @param ean13 The ISSN code to convert
138      * @return A valid ISSN code or <code>null</code>
139      * if the input ISSN EAN-13 code is not valid
140      * @since 1.7
141      */
142     public String extractFromEAN13(final String ean13) {
143         String input = ean13.trim();
144         if (input.length() != EAN_ISSN_LEN ) {
145             throw new IllegalArgumentException("Invalid length " + input.length() + " for '" + input + "'");
146         }
147         if (!input.startsWith(ISSN_PREFIX)) {
148             throw new IllegalArgumentException("Prefix must be " + ISSN_PREFIX + " to contain an ISSN: '" + ean13 + "'");
149         }
150         final Object result = validateEan(input);
151         if (result == null) {
152             return null;
153         }
154         // Calculate the ISSN code
155         input = result.toString();
156         try {
157             //CHECKSTYLE:OFF: MagicNumber
158             final String issnBase = input.substring(3, 10); // TODO: how to derive these
159             //CHECKSTYLE:ON: MagicNumber
160             final String checkDigit = ISSNCheckDigit.ISSN_CHECK_DIGIT.calculate(issnBase);
161             final String issn = issnBase + checkDigit;
162             return issn;
163         } catch (final CheckDigitException e) { // Should not happen
164             throw new IllegalArgumentException("Check digit error for '" + ean13 + "' - " + e.getMessage());
165         }
166     }
167 
168     /**
169      * Check the code is a valid ISSN code after any transformation
170      * by the validate routine.
171      * @param code The code to validate.
172      * @return {@code true} if a valid ISSN
173      * code, otherwise {@code false}.
174      */
175     public boolean isValid(final String code) {
176         return VALIDATOR.isValid(code);
177     }
178 
179     /**
180      * Check the code is valid ISSN code.
181      * <p>
182      * If valid, this method returns the ISSN code with
183      * the 'ISSN ' prefix removed (if it was present)
184      *
185      * @param code The code to validate.
186      * @return A valid ISSN code if valid, otherwise <code>null</code>.
187      */
188     public Object validate(final String code) {
189         return VALIDATOR.validate(code);
190     }
191 
192     /**
193      * Check the code is a valid EAN code.
194      * <p>
195      * If valid, this method returns the EAN code
196      *
197      * @param code The code to validate.
198      * @return A valid EAN code if valid, otherwise <code>null</code>.
199      * @since 1.7
200      */
201     public Object validateEan(final String code) {
202         return EAN_VALIDATOR.validate(code);
203     }
204 }