001 002package org.xbib.standardnumber; 003 004import javax.xml.stream.XMLEventReader; 005import javax.xml.stream.XMLInputFactory; 006import javax.xml.stream.XMLStreamException; 007import javax.xml.stream.events.Characters; 008import javax.xml.stream.events.EndElement; 009import javax.xml.stream.events.StartElement; 010import javax.xml.stream.events.XMLEvent; 011import java.io.InputStream; 012import java.util.ArrayList; 013import java.util.List; 014import java.util.Stack; 015import java.util.regex.Matcher; 016import java.util.regex.Pattern; 017 018/** 019 * ISO 2108 International Standard Book Number (ISBN) 020 * 021 * Z39.50 BIB-1 Use Attribute 7 022 * 023 * The International Standard Book Number is a 13-digit number 024 * that uniquely identifies books and book-like products published 025 * internationally. 026 * 027 * The purpose of the ISBN is to establish and identify one title or 028 * edition of a title from one specific publisher 029 * and is unique to that edition, allowing for more efficient marketing of products by booksellers, 030 * libraries, universities, wholesalers and distributors. 031 * 032 * Every ISBN consists of thirteen digits and whenever it is printed it is preceded by the letters ISBN. 033 * The thirteen-digit number is divided into four parts of variable length, each part separated by a hyphen. 034 * 035 * This class is based upon the ISBN converter and formatter class 036 * Copyright 2000-2005 by Openly Informatics, Inc. http://www.openly.com/ 037 * 038 * @see <a href="http://www.s.org/standards/home/s/international/html/usm12.htm">The ISBN Users' Manual</a> 039 * @see <a href="http://www.ietf.org/html.charters/OLD/urn-charter.html">The IETF URN Charter</a> 040 * @see <a href="http://www.iana.org/assignments/urn-namespaces">The IANA URN assignments</a> 041 * @see <a href="http://www.isbn-international.org/download/List%20of%20Ranges.pdf">ISBN prefix list</a> 042 */ 043public class ISBN extends AbstractStandardNumber implements Comparable<ISBN>, StandardNumber { 044 045 private static final Pattern PATTERN = Pattern.compile("[\\p{Digit}xX\\-]{10,17}"); 046 047 private static final List<String> ranges = new ISBNRangeMessageConfigurator().getRanges(); 048 049 private String value; 050 051 private boolean createWithChecksum; 052 053 private String eanvalue; 054 055 private boolean eanPreferred; 056 057 private boolean valid; 058 059 private boolean isEAN; 060 061 @Override 062 public String type() { 063 return "isbn"; 064 } 065 066 /** 067 * Set ISBN value 068 * 069 * @param value the ISBN candidate string 070 */ 071 @Override 072 public ISBN set(CharSequence value) { 073 this.value = value != null ? value.toString() : null; 074 return this; 075 } 076 077 @Override 078 public ISBN createChecksum(boolean createWithChecksum) { 079 this.createWithChecksum = createWithChecksum; 080 return this; 081 } 082 083 @Override 084 public int compareTo(ISBN isbn) { 085 return value != null ? value.compareTo(isbn.normalizedValue()): -1; 086 } 087 088 @Override 089 public ISBN normalize() { 090 Matcher m = PATTERN.matcher(value); 091 this.value = m.find() ? dehyphenate(value.substring(m.start(), m.end())) : null; 092 return this; 093 } 094 095 /** 096 * Check for this ISBN number validity 097 * 098 * @return true if valid, false otherwise 099 */ 100 @Override 101 public boolean isValid() throws NumberFormatException { 102 return value != null && !value.isEmpty() && check() && (eanPreferred ? eanvalue != null : value != null); 103 } 104 105 @Override 106 public ISBN verify() throws NumberFormatException { 107 if (value == null || value.isEmpty()) { 108 throw new NumberFormatException("must not be null"); 109 } 110 check(); 111 this.valid = eanPreferred ? eanvalue != null : value != null; 112 if (!valid) { 113 throw new NumberFormatException("invalid number"); 114 } 115 return this; 116 } 117 118 /** 119 * Get the normalized value of this standard book number 120 * 121 * @return the value of this standard book number 122 */ 123 @Override 124 public String normalizedValue() { 125 return eanPreferred ? eanvalue : value; 126 } 127 128 /** 129 * Get printable representation of this standard book number 130 * 131 * @return ISBN-13, with (fixed) check digit 132 */ 133 @Override 134 public String format() { 135 if ((!eanPreferred && value == null) || eanvalue == null) { 136 return null; 137 } 138 return eanPreferred ? 139 fix(eanvalue != null ? eanvalue : "978" + value) : 140 fix("978" + value).substring(4); 141 } 142 143 @Override 144 public ISBN reset() { 145 this.value = null; 146 this.createWithChecksum = false; 147 this.eanvalue = null; 148 this.eanPreferred = false; 149 this.valid = false; 150 this.isEAN = false; 151 return this; 152 } 153 154 public boolean isEAN() { 155 return isEAN; 156 } 157 158 /** 159 * Prefer European Article Number (EAN, ISBN-13) 160 */ 161 public ISBN ean(boolean preferEAN) { 162 this.eanPreferred = preferEAN; 163 return this; 164 } 165 166 /** 167 * Get country and publisher code 168 * 169 * @return the country/publisher code from ISBN 170 */ 171 public String getCountryAndPublisherCode() { 172 // we don't care about the wrong createChecksum when we fix the value 173 String code = eanvalue != null ? fix(eanvalue) : fix("978" + value); 174 String s = code.substring(4); 175 int pos1 = s.indexOf('-'); 176 if (pos1 <= 0) { 177 return null; 178 } 179 String pubCode = s.substring(pos1 + 1); 180 int pos2 = pubCode.indexOf('-'); 181 if (pos2 <= 0) { 182 return null; 183 } 184 return code.substring(0, pos1 + pos2 + 5); 185 } 186 187 private String hyphenate(String prefix, String isbn) { 188 StringBuilder sb = new StringBuilder(prefix.substring(0, 4)); // '978-', '979-' 189 prefix = prefix.substring(4); 190 isbn = isbn.substring(3); // 978, 979 191 int i = 0; 192 int j = 0; 193 while (i < prefix.length()) { 194 char ch = prefix.charAt(i++); 195 if (ch == '-') { 196 sb.append('-'); // set first hyphen 197 } else { 198 sb.append(isbn.charAt(j++)); 199 } 200 } 201 sb.append('-'); // set second hyphen 202 while (j < (isbn.length() - 1)) { 203 sb.append(isbn.charAt(j++)); 204 } 205 sb.append('-'); // set third hyphen 206 sb.append(isbn.charAt(isbn.length() - 1)); 207 return sb.toString(); 208 } 209 210 private boolean check() { 211 this.eanvalue = null; 212 this.isEAN = false; 213 int i; 214 int val; 215 if (value.length() < 9) { 216 return false; 217 } 218 if (value.length() == 10) { 219 // ISBN-10 220 int checksum = 0; 221 int weight = 10; 222 for (i = 0; weight > 0; i++) { 223 val = value.charAt(i) == 'X' || value.charAt(i) == 'x' ? 10 224 : value.charAt(i) - '0'; 225 if (val >= 0) { 226 if (val == 10 && weight != 1) { 227 return false; 228 } 229 checksum += weight * val; 230 weight--; 231 } else { 232 return false; 233 } 234 } 235 String s = value.substring(0, 9); 236 if (checksum % 11 != 0) { 237 if (createWithChecksum) { 238 this.value = s + createCheckDigit10(s); 239 } else { 240 return false; 241 } 242 } 243 this.eanvalue = "978" + s + createCheckDigit13("978" + s); 244 } else if (value.length() == 13) { 245 // ISBN-13 "book land" 246 if (!value.startsWith("978") && !value.startsWith("979")) { 247 return false; 248 } 249 int checksum13 = 0; 250 int weight13 = 1; 251 for (i = 0; i < 13; i++) { 252 val = value.charAt(i) == 'X' || value.charAt(i) == 'x' ? 10 : value.charAt(i) - '0'; 253 if (val >= 0) { 254 if (val == 10) { 255 return false; 256 } 257 checksum13 += (weight13 * val); 258 weight13 = (weight13 + 2) % 4; 259 } else { 260 return false; 261 } 262 } 263 // set value 264 if ((checksum13 % 10) != 0) { 265 if (eanPreferred && createWithChecksum) { 266 // with createChecksum 267 eanvalue = value.substring(0, 12) + createCheckDigit13(value.substring(0, 12)); 268 } else { 269 return false; 270 } 271 } else { 272 eanvalue = value; 273 } 274 if (!eanPreferred && (eanvalue.startsWith("978") || eanvalue.startsWith("979"))) { 275 // create 10-digit from 13-digit 276 this.value = eanvalue.substring(3, 12) + createCheckDigit10(eanvalue.substring(3, 12)); 277 } else { 278 // 10 digit version not available - not an error 279 this.value = null; 280 } 281 this.isEAN = true; 282 } else if (value.length() == 9) { 283 String s = value.substring(0, 9); 284 // repair ISBN-10 ? 285 if (createWithChecksum) { 286 // create 978 from 10-digit without createChecksum 287 eanvalue = "978" + s + createCheckDigit13("978" + s); 288 value = s + createCheckDigit10(s); 289 } else { 290 return false; 291 } 292 } else if (value.length() == 12) { 293 // repair ISBN-13 ? 294 if (!value.startsWith("978") && !value.startsWith("979")) { 295 return false; 296 } 297 if (createWithChecksum) { 298 String s = value.substring(0, 9); 299 String t = value.substring(3, 12); 300 // create 978 from 10-digit 301 this.eanvalue = "978" + s + createCheckDigit13("978" + s); 302 this.value = t + createCheckDigit10(t); 303 } else { 304 return false; 305 } 306 this.isEAN = true; 307 } else { 308 return false; 309 } 310 return true; 311 } 312 313 /** 314 * Returns a ISBN check digit for the first 9 digits in a string 315 * 316 * @param value the value 317 * @return check digit 318 * 319 * @throws NumberFormatException 320 */ 321 private char createCheckDigit10(String value) throws NumberFormatException { 322 int checksum = 0; 323 int val; 324 int l = value.length(); 325 for (int i = 0; i < l; i++) { 326 val = value.charAt(i) - '0'; 327 if (val < 0 || val > 9) { 328 throw new NumberFormatException("not a digit in " + value); 329 } 330 checksum += val * (10-i); 331 } 332 int mod = checksum % 11; 333 return mod == 0 ? '0' : mod == 1 ? 'X' : (char)((11-mod) + '0'); 334 } 335 336 /** 337 * Returns an ISBN check digit for the first 12 digits in a string 338 * 339 * @param value the value 340 * @return check digit 341 * 342 * @throws NumberFormatException 343 */ 344 private char createCheckDigit13(String value) throws NumberFormatException { 345 int checksum = 0; 346 int weight; 347 int val; 348 int l = value.length(); 349 for (int i = 0; i < l; i++) { 350 val = value.charAt(i) - '0'; 351 if (val < 0 || val > 9) { 352 throw new NumberFormatException("not a digit in " + value); 353 } 354 weight = i % 2 == 0 ? 1 : 3; 355 checksum += weight * val; 356 } 357 int mod = 10 - checksum % 10; 358 return mod == 10 ? '0' : (char)(mod + '0'); 359 } 360 361 private String fix(String isbn) { 362 if (isbn == null) { 363 return null; 364 } 365 for (int i = 0; i < ranges.size(); i += 2) { 366 if (isInRange(isbn, ranges.get(i), ranges.get(i + 1)) == 0) { 367 return hyphenate(ranges.get(i), isbn); 368 } 369 } 370 return isbn; 371 } 372 373 /** 374 * Check if ISBN is within a given value range 375 * @param isbn ISBN to check 376 * @param begin lower ISBN 377 * @param end higher ISBN 378 * @return -1 if too low, 1 if too high, 0 if range matches 379 */ 380 private int isInRange(String isbn, String begin, String end) { 381 String b = dehyphenate(begin); 382 int blen = b.length(); 383 int c = blen <= isbn.length() ? 384 isbn.substring(0, blen).compareTo(b) : 385 isbn.compareTo(b); 386 if (c < 0) { 387 return -1; 388 } 389 String e = dehyphenate(end); 390 int elen = e.length(); 391 c = e.compareTo(isbn.substring(0, elen)); 392 if (c < 0) { 393 return 1; 394 } 395 return 0; 396 } 397 398 private String dehyphenate(String isbn) { 399 StringBuilder sb = new StringBuilder(isbn); 400 int i = sb.indexOf("-"); 401 while (i >= 0) { 402 sb.deleteCharAt(i); 403 i = sb.indexOf("-"); 404 } 405 return sb.toString(); 406 } 407 408 private final static class ISBNRangeMessageConfigurator { 409 410 private final Stack<StringBuilder> content; 411 412 private final List<String> ranges; 413 414 private String prefix; 415 416 private String rangeBegin; 417 418 private String rangeEnd; 419 420 private int length; 421 422 private boolean valid; 423 424 public ISBNRangeMessageConfigurator() { 425 content = new Stack<StringBuilder>(); 426 ranges = new ArrayList<String>(); 427 length = 0; 428 try { 429 InputStream in = getClass().getResourceAsStream("/org/xbib/standardnumber/RangeMessage.xml"); 430 XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); 431 XMLEventReader xmlReader = xmlInputFactory.createXMLEventReader(in); 432 while (xmlReader.hasNext()) { 433 processEvent(xmlReader.peek()); 434 xmlReader.nextEvent(); 435 } 436 } catch (XMLStreamException e) { 437 throw new RuntimeException(e.getMessage()); 438 } 439 } 440 441 private void processEvent(XMLEvent e) { 442 switch (e.getEventType()) { 443 case XMLEvent.START_ELEMENT: { 444 StartElement element = e.asStartElement(); 445 String name = element.getName().getLocalPart(); 446 if ("RegistrationGroups".equals(name)) { 447 valid = true; 448 } 449 content.push(new StringBuilder()); 450 break; 451 } 452 case XMLEvent.END_ELEMENT: { 453 EndElement element = e.asEndElement(); 454 String name = element.getName().getLocalPart(); 455 String v = content.pop().toString(); 456 if ("Prefix".equals(name)) { 457 prefix = v; 458 } 459 if ("Range".equals(name)) { 460 int pos = v.indexOf('-'); 461 if (pos > 0) { 462 rangeBegin = v.substring(0, pos); 463 rangeEnd = v.substring(pos + 1); 464 } 465 } 466 if ("Length".equals(name)) { 467 length = Integer.parseInt(v); 468 } 469 if ("Rule".equals(name)) { 470 if (valid && rangeBegin != null && rangeEnd != null) { 471 if (length > 0) { 472 ranges.add(prefix + "-" + rangeBegin.substring(0, length)); 473 ranges.add(prefix + "-" + rangeEnd.substring(0, length)); 474 } 475 } 476 } 477 break; 478 } 479 case XMLEvent.CHARACTERS: { 480 Characters c = (Characters) e; 481 if (!c.isIgnorableWhiteSpace()) { 482 String text = c.getData().trim(); 483 if (text.length() > 0 && !content.empty()) { 484 content.peek().append(text); 485 } 486 } 487 break; 488 } 489 } 490 } 491 492 public List<String> getRanges() { 493 return ranges; 494 } 495 } 496 497} 498 499