001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.validation.validator; 018 019import org.apache.wicket.validation.IValidatable; 020import org.apache.wicket.validation.IValidationError; 021import org.apache.wicket.validation.IValidator; 022import org.apache.wicket.validation.ValidationError; 023 024/** 025 * Checks if a credit card number is valid. The number will be checked for "American Express", 026 * "China UnionPay", "Diners Club Carte Blanche", "Diners Club International", 027 * "Diners Club US & Canada", "Discover Card", "JCB", "Laser", "Maestro", "MasterCard", "Solo", 028 * "Switch", "Visa" and "Visa Electron". If none of those apply to the credit card number, the 029 * credit card number is considered invalid. 030 * 031 * <p> 032 * Card prefixes and lengths have been taken from <a 033 * href="http://en.wikipedia.org/w/index.php?title=Bank_card_number&oldid=322132931">Wikipedia</a>. 034 * 035 * @author Johan Compagner 036 * @author Joachim F. Rohde 037 * @since 1.2.6 038 */ 039public class CreditCardValidator implements IValidator<String> 040{ 041 private static final long serialVersionUID = 1L; 042 043 /** */ 044 public static enum CreditCard { 045 /** */ 046 INVALID(null), 047 /** */ 048 AMERICAN_EXPRESS("American Express"), 049 /** */ 050 CHINA_UNIONPAY("China UnionPay"), 051 /** */ 052 DINERS_CLUB_CARTE_BLANCHE("Diners Club Carte Blanche"), 053 /** */ 054 DINERS_CLUB_INTERNATIONAL("Diners Club International"), 055 /** */ 056 DINERS_CLUB_US_AND_CANADA("Diners Club US & Canada"), 057 /** */ 058 DISCOVER_CARD("Discover Card"), 059 /** */ 060 JCB("JCB"), 061 /** */ 062 LASER("Laser"), 063 /** */ 064 MAESTRO("Maestro"), 065 /** */ 066 MASTERCARD("MasterCard"), 067 /** */ 068 SOLO("Solo"), 069 /** */ 070 SWITCH("Switch"), 071 /** */ 072 VISA("Visa"), 073 /** */ 074 VISA_ELECTRON("Visa Electron"); 075 076 private final String name; 077 078 CreditCard(String name) 079 { 080 this.name = name; 081 } 082 } 083 084 /** The ID which represents the credit card institute. */ 085 private CreditCard cardId = CreditCard.INVALID; 086 087 private boolean failOnUnknown = true; 088 089 /** 090 * Construct. 091 */ 092 public CreditCardValidator() 093 { 094 } 095 096 /** 097 * Construct. 098 * 099 * @param failOnUnknown 100 */ 101 public CreditCardValidator(final boolean failOnUnknown) 102 { 103 this.failOnUnknown = failOnUnknown; 104 } 105 106 /** 107 * 108 * @return Credit card issuer 109 */ 110 public final CreditCard getCardId() 111 { 112 return cardId; 113 } 114 115 /** 116 * Allow subclasses to set the card id 117 * 118 * @param cardId 119 */ 120 protected void setCardId(final CreditCard cardId) 121 { 122 this.cardId = cardId; 123 } 124 125 @Override 126 public void validate(final IValidatable<String> validatable) 127 { 128 final String value = validatable.getValue(); 129 130 try 131 { 132 if (!isLengthAndPrefixCorrect(value)) 133 { 134 validatable.error(decorate(new ValidationError(this), validatable)); 135 } 136 } 137 catch (final NumberFormatException ex) 138 { 139 validatable.error(decorate(new ValidationError(this), validatable)); 140 } 141 } 142 143 /** 144 * Allows subclasses to decorate reported errors 145 * 146 * @param error 147 * @param validatable 148 * @return decorated error 149 */ 150 protected IValidationError decorate(IValidationError error, IValidatable<String> validatable) 151 { 152 return error; 153 } 154 155 /** 156 * Checks if the credit card number can be determined as a valid number. 157 * 158 * @param creditCardNumber 159 * the credit card number as a string 160 * @return <code>TRUE</code> if the credit card number could be determined as a valid number, 161 * else <code>FALSE</code> is returned 162 */ 163 protected boolean isLengthAndPrefixCorrect(String creditCardNumber) 164 { 165 if (creditCardNumber == null) 166 { 167 return false; 168 } 169 170 // strip spaces and dashes 171 creditCardNumber = creditCardNumber.replaceAll("[ -]", ""); 172 173 // the length of the credit card number has to be between 12 and 19. 174 // else the number is invalid. 175 if ((creditCardNumber.length() >= 12) && (creditCardNumber.length() <= 19) && 176 isChecksumCorrect(creditCardNumber)) 177 { 178 if ((failOnUnknown == false) || 179 (determineCardId(creditCardNumber) != CreditCard.INVALID)) 180 { 181 return true; 182 } 183 } 184 185 return false; 186 } 187 188 /** 189 * Checks if the credit card number can be determined as a valid number. 190 * 191 * @param creditCardNumber 192 * the credit card number as a string 193 * @return <code>TRUE</code> if the credit card number could be determined as a valid number, 194 * else <code>FALSE</code> is returned 195 */ 196 public final CreditCard determineCardId(String creditCardNumber) 197 { 198 if (creditCardNumber == null) 199 { 200 return CreditCard.INVALID; 201 } 202 203 // strip spaces and dashes 204 creditCardNumber = creditCardNumber.replaceAll("[ -]", ""); 205 206 // the length of the credit card number has to be between 12 and 19. 207 // else the number is invalid. 208 if ((creditCardNumber.length() >= 12) && (creditCardNumber.length() <= 19) && 209 isChecksumCorrect(creditCardNumber)) 210 { 211 cardId = CreditCard.INVALID; 212 if (cardId == CreditCard.INVALID) 213 { 214 cardId = isAmericanExpress(creditCardNumber); 215 } 216 if (cardId == CreditCard.INVALID) 217 { 218 cardId = isChinaUnionPay(creditCardNumber); 219 } 220 if (cardId == CreditCard.INVALID) 221 { 222 cardId = isDinersClubCarteBlanche(creditCardNumber); 223 } 224 if (cardId == CreditCard.INVALID) 225 { 226 cardId = isDinersClubInternational(creditCardNumber); 227 } 228 if (cardId == CreditCard.INVALID) 229 { 230 cardId = isDinersClubUsAndCanada(creditCardNumber); 231 } 232 if (cardId == CreditCard.INVALID) 233 { 234 cardId = isDiscoverCard(creditCardNumber); 235 } 236 if (cardId == CreditCard.INVALID) 237 { 238 cardId = isJCB(creditCardNumber); 239 } 240 if (cardId == CreditCard.INVALID) 241 { 242 cardId = isLaser(creditCardNumber); 243 } 244 if (cardId == CreditCard.INVALID) 245 { 246 cardId = isMaestro(creditCardNumber); 247 } 248 if (cardId == CreditCard.INVALID) 249 { 250 cardId = isMastercard(creditCardNumber); 251 } 252 if (cardId == CreditCard.INVALID) 253 { 254 cardId = isSolo(creditCardNumber); 255 } 256 if (cardId == CreditCard.INVALID) 257 { 258 cardId = isSwitch(creditCardNumber); 259 } 260 if (cardId == CreditCard.INVALID) 261 { 262 cardId = isVisa(creditCardNumber); 263 } 264 if (cardId == CreditCard.INVALID) 265 { 266 cardId = isVisaElectron(creditCardNumber); 267 } 268 } 269 else 270 { 271 cardId = isUnknown(creditCardNumber); 272 } 273 274 return cardId; 275 } 276 277 /** 278 * Can be used (subclassed) to extend the test with a credit card not yet known by the 279 * validator. 280 * 281 * @param creditCardNumber 282 * the credit card number as a string 283 * @return The credit card id of the issuer 284 */ 285 protected CreditCard isUnknown(String creditCardNumber) 286 { 287 return CreditCard.INVALID; 288 } 289 290 /** 291 * Check if the credit card is an American Express. An American Express number has to start with 292 * 34 or 37 and has to have a length of 15. The number has to be validated with the Luhn 293 * algorithm. 294 * 295 * @param creditCardNumber 296 * the credit card number as a string 297 * @return The credit card id of the issuer 298 */ 299 private CreditCard isAmericanExpress(String creditCardNumber) 300 { 301 if (creditCardNumber.length() == 15 && 302 (creditCardNumber.startsWith("34") || creditCardNumber.startsWith("37"))) 303 { 304 return CreditCard.AMERICAN_EXPRESS; 305 } 306 307 return CreditCard.INVALID; 308 } 309 310 /** 311 * Check if the credit card is a China UnionPay. A China UnionPay number has to start with 622 312 * (622126-622925) and has to have a length between 16 and 19. No further validation takes 313 * place.<br/> 314 * <br/> 315 * 316 * @param creditCardNumber 317 * the credit card number as a string 318 * @return The credit card id of the issuer 319 */ 320 private CreditCard isChinaUnionPay(String creditCardNumber) 321 { 322 if ((creditCardNumber.length() >= 16 && creditCardNumber.length() <= 19) && 323 (creditCardNumber.startsWith("622"))) 324 { 325 int firstDigits = Integer.parseInt(creditCardNumber.substring(0, 6)); 326 if (firstDigits >= 622126 && firstDigits <= 622925) 327 { 328 return CreditCard.CHINA_UNIONPAY; 329 } 330 } 331 332 return CreditCard.INVALID; 333 } 334 335 /** 336 * Check if the credit card is a Diners Club Carte Blanche. A Diners Club Carte Blanche number 337 * has to start with a number between 300 and 305 and has to have a length of 14. The number has 338 * to be validated with the Luhn algorithm. 339 * 340 * @param creditCardNumber 341 * the credit card number as a string 342 * @return The credit card id of the issuer 343 */ 344 private CreditCard isDinersClubCarteBlanche(String creditCardNumber) 345 { 346 if (creditCardNumber.length() == 14 && creditCardNumber.startsWith("30")) 347 { 348 int firstDigits = Integer.parseInt(creditCardNumber.substring(0, 3)); 349 if (firstDigits >= 300 && firstDigits <= 305) 350 { 351 return CreditCard.DINERS_CLUB_CARTE_BLANCHE; 352 } 353 } 354 355 return CreditCard.INVALID; 356 } 357 358 /** 359 * Check if the credit card is a Diners Club International. A Diners Club International number 360 * has to start with the number 36 and has to have a length of 14. The number has to be 361 * validated with the Luhn algorithm. 362 * 363 * @param creditCardNumber 364 * the credit card number as a string 365 * @return The credit card id of the issuer 366 */ 367 private CreditCard isDinersClubInternational(String creditCardNumber) 368 { 369 if (creditCardNumber.length() == 14 && creditCardNumber.startsWith("36")) 370 { 371 return CreditCard.DINERS_CLUB_INTERNATIONAL; 372 } 373 374 return CreditCard.INVALID; 375 } 376 377 /** 378 * Check if the credit card is a Diners Club US & Canada. A Diners Club US & Canada number has 379 * to start with the number 54 or 55 and has to have a length of 16. The number has to be 380 * validated with the Luhn algorithm. 381 * 382 * @param creditCardNumber 383 * the credit card number as a string 384 * @return The credit card id of the issuer 385 */ 386 private CreditCard isDinersClubUsAndCanada(String creditCardNumber) 387 { 388 if (creditCardNumber.length() == 16 && 389 (creditCardNumber.startsWith("54") || creditCardNumber.startsWith("55"))) 390 { 391 return CreditCard.DINERS_CLUB_US_AND_CANADA; 392 } 393 394 return CreditCard.INVALID; 395 } 396 397 /** 398 * Check if the credit card is a Discover Card. A Discover Card number has to start with 6011, 399 * 622126-622925, 644-649 or 65 and has to have a length of 16. The number has to be validated 400 * with the Luhn algorithm. 401 * 402 * @param creditCardNumber 403 * the credit card number as a string 404 * @return The credit card id of the issuer 405 */ 406 private CreditCard isDiscoverCard(String creditCardNumber) 407 { 408 if (creditCardNumber.length() == 16 && creditCardNumber.startsWith("6")) 409 { 410 int firstThreeDigits = Integer.parseInt(creditCardNumber.substring(0, 3)); 411 int firstSixDigits = Integer.parseInt(creditCardNumber.substring(0, 6)); 412 if (creditCardNumber.startsWith("6011") || creditCardNumber.startsWith("65") || 413 (firstThreeDigits >= 644 && firstThreeDigits <= 649) || 414 (firstSixDigits >= 622126 && firstSixDigits <= 622925)) 415 { 416 return CreditCard.DISCOVER_CARD; 417 } 418 } 419 420 return CreditCard.INVALID; 421 } 422 423 /** 424 * Check if the credit card is a JCB. A JCB number has to start with a number between 3528 and 425 * 3589 and has to have a length of 16. The number has to be validated with the Luhn algorithm. 426 * 427 * @param creditCardNumber 428 * the credit card number as a string 429 * @return The credit card id of the issuer 430 */ 431 private CreditCard isJCB(String creditCardNumber) 432 { 433 if (creditCardNumber.length() == 16) 434 { 435 int firstFourDigits = Integer.parseInt(creditCardNumber.substring(0, 4)); 436 if (firstFourDigits >= 3528 && firstFourDigits <= 3589) 437 { 438 return CreditCard.JCB; 439 } 440 } 441 442 return CreditCard.INVALID; 443 } 444 445 /** 446 * Check if the credit card is a Laser. A Laser number has to start with 6304, 6706, 6771 or 447 * 6709 and has to have a length between 16 and 19 digits. The number has to be validated with 448 * the Luhn algorithm. 449 * 450 * @param creditCardNumber 451 * the credit card number as a string 452 * @return The credit card id of the issuer 453 */ 454 private CreditCard isLaser(String creditCardNumber) 455 { 456 if (creditCardNumber.length() >= 16 && creditCardNumber.length() <= 19) 457 { 458 if (creditCardNumber.startsWith("6304") || creditCardNumber.startsWith("6706") || 459 creditCardNumber.startsWith("6771") || creditCardNumber.startsWith("6709")) 460 { 461 return CreditCard.LASER; 462 } 463 } 464 465 return CreditCard.INVALID; 466 } 467 468 /** 469 * Check if the credit card is a Maestro. A Maestro number has to start with 470 * 5018,5020,5038,6304,6759,6761 or 6763 and has to have a length between 12 and 19 digits. The 471 * number has to be validated with the Luhn algorithm. 472 * 473 * @param creditCardNumber 474 * the credit card number as a string 475 * @return The credit card id of the issuer 476 */ 477 private CreditCard isMaestro(String creditCardNumber) 478 { 479 if (creditCardNumber.length() >= 12 && creditCardNumber.length() <= 19) 480 { 481 if (creditCardNumber.startsWith("5018") || creditCardNumber.startsWith("5020") || 482 creditCardNumber.startsWith("5038") || creditCardNumber.startsWith("6304") || 483 creditCardNumber.startsWith("6759") || creditCardNumber.startsWith("6761") || 484 creditCardNumber.startsWith("6763")) 485 { 486 return CreditCard.MAESTRO; 487 } 488 } 489 490 return CreditCard.INVALID; 491 } 492 493 /** 494 * Check if the credit card is a Solo. A Solo number has to start with 6334 or 6767 and has to 495 * have a length of 16, 18 or 19 digits. The number has to be validated with the Luhn algorithm. 496 * 497 * @param creditCardNumber 498 * the credit card number as a string 499 * @return The credit card id of the issuer 500 */ 501 private CreditCard isSolo(String creditCardNumber) 502 { 503 if ((creditCardNumber.length() == 16) || (creditCardNumber.length() == 18) || 504 (creditCardNumber.length() == 19)) 505 { 506 if (creditCardNumber.startsWith("6334") || creditCardNumber.startsWith("6767")) 507 { 508 return CreditCard.SOLO; 509 } 510 } 511 512 return CreditCard.INVALID; 513 } 514 515 /** 516 * Check if the credit card is a Switch. A Switch number has to start with 517 * 4903,4905,4911,4936,564182,633110,6333 or 6759 and has to have a length of 16, 18 or 19 518 * digits. The number has to be validated with the Luhn algorithm. 519 * 520 * @param creditCardNumber 521 * the credit card number as a string 522 * @return The credit card id of the issuer 523 */ 524 private CreditCard isSwitch(String creditCardNumber) 525 { 526 if ((creditCardNumber.length() == 16 || creditCardNumber.length() == 18 || creditCardNumber.length() == 19)) 527 { 528 if (creditCardNumber.startsWith("4903") || creditCardNumber.startsWith("4905") || 529 creditCardNumber.startsWith("4911") || creditCardNumber.startsWith("4936") || 530 creditCardNumber.startsWith("564182") || creditCardNumber.startsWith("633110") || 531 creditCardNumber.startsWith("6333") || creditCardNumber.startsWith("6759")) 532 { 533 return CreditCard.SWITCH; 534 } 535 } 536 537 return CreditCard.INVALID; 538 } 539 540 /** 541 * Check if the credit card is a Visa. A Visa number has to start with a 4 and has to have a 542 * length of 13 or 16 digits. The number has to be validated with the Luhn algorithm. 543 * 544 * @param creditCardNumber 545 * the credit card number as a string 546 * @return The credit card id of the issuer 547 */ 548 private CreditCard isVisa(String creditCardNumber) 549 { 550 if (creditCardNumber.length() == 13 || creditCardNumber.length() == 16) 551 { 552 if (creditCardNumber.startsWith("4")) 553 { 554 return CreditCard.VISA; 555 } 556 } 557 558 return CreditCard.INVALID; 559 } 560 561 /** 562 * Check if the credit card is a Visa Electron. A Visa Electron number has to start with 563 * 417500,4917,4913,4508 or 4844 and has to have a length of 16 digits. The number has to be 564 * validated with the Luhn algorithm. 565 * 566 * @param creditCardNumber 567 * the credit card number as a string 568 * @return The credit card id of the issuer 569 */ 570 private CreditCard isVisaElectron(String creditCardNumber) 571 { 572 if (creditCardNumber.length() == 16 && 573 (creditCardNumber.startsWith("417500") || creditCardNumber.startsWith("4917") || 574 creditCardNumber.startsWith("4913") || creditCardNumber.startsWith("4508") || creditCardNumber.startsWith("4844"))) 575 { 576 return CreditCard.VISA_ELECTRON; 577 } 578 579 return CreditCard.INVALID; 580 } 581 582 /** 583 * Check if the credit card is a Mastercard. A Mastercard number has to start with a number 584 * between 51 and 55 and has to have a length of 16. The number has to be validated with the 585 * Luhn algorithm. 586 * 587 * @param creditCardNumber 588 * the credit card number as a string 589 * @return The credit card id of the issuer 590 */ 591 private CreditCard isMastercard(String creditCardNumber) 592 { 593 if (creditCardNumber.length() == 16) 594 { 595 int firstTwoDigits = Integer.parseInt(creditCardNumber.substring(0, 2)); 596 if (firstTwoDigits >= 51 && firstTwoDigits <= 55) 597 { 598 return CreditCard.MASTERCARD; 599 } 600 } 601 602 return CreditCard.INVALID; 603 } 604 605 /** 606 * Calculates the checksum of a credit card number using the Luhn algorithm (the so-called 607 * "mod 10" algorithm). 608 * 609 * @param creditCardNumber 610 * the credit card number for which the checksum should be calculated 611 * @return <code>TRUE</code> if the checksum for the given credit card number is valid, else 612 * return <code>FALSE</code> 613 * @see <a href="http://en.wikipedia.org/wiki/Luhn_algorithm">Wikipedie - Luhn algorithm</a> 614 */ 615 protected final boolean isChecksumCorrect(final String creditCardNumber) 616 { 617 int nulOffset = '0'; 618 int sum = 0; 619 for (int i = 1; i <= creditCardNumber.length(); i++) 620 { 621 int currentDigit = creditCardNumber.charAt(creditCardNumber.length() - i) - nulOffset; 622 if ((i % 2) == 0) 623 { 624 currentDigit *= 2; 625 currentDigit = currentDigit > 9 ? currentDigit - 9 : currentDigit; 626 sum += currentDigit; 627 } 628 else 629 { 630 sum += currentDigit; 631 } 632 } 633 634 return (sum % 10) == 0; 635 } 636}