001 package railo.runtime.op.validators; 002 003 import railo.commons.lang.StringUtil; 004 import railo.runtime.exp.ExpressionException; 005 import railo.runtime.op.Caster; 006 007 /** 008 * logic to determine if a credit card number is valid. no GUI, just 009 * calculation. 010 * 011 */ 012 public final class ValidateCreditCard { 013 014 /** 015 * enum for Amex 016 */ 017 static final int AMEX = 1; 018 019 /** 020 * true if debugging output wanted 021 */ 022 static final boolean DEBUGGING = true; 023 024 /** 025 * enum for Diner's club 026 */ 027 static final int DINERS = 2; // includes Carte Blanche 028 029 /** 030 * enum for Discover Card 031 */ 032 static final int DISCOVER = 3; 033 034 /** 035 * enum for Enroute 036 */ 037 static final int ENROUTE = 4; 038 039 /** 040 * enum for JCB 041 */ 042 static final int JCB = 5; 043 044 /** 045 * enum for Mastercard 046 */ 047 static final int MASTERCARD = 6; 048 049 /** 050 * enum for insufficient digits 051 */ 052 static final int NOT_ENOUGH_DIGITS = -3; 053 054 /** 055 * enum for too many digits 056 */ 057 static final int TOO_MANY_DIGITS = -2; 058 059 /** 060 * enum for unknown vendor 061 */ 062 static final int UNKNOWN_VENDOR = -1; 063 064 /** 065 * enum for Visa 066 */ 067 static final int VISA = 7; 068 069 /** 070 * Used to speed up findMatchingRange by caching the last hit. 071 */ 072 private static int cachedLastFind = 0; 073 074 /** 075 * ranges of credit card number that belong to each company. buildRanges 076 * initialises. 077 */ 078 private static LCR[] ranges; 079 080 /** 081 * used by vendorToString to describe the enumerations 082 */ 083 private static final String[] vendors = 084 {"Error: not enough digits", 085 "Error: too many digits", 086 "Error: unknown credit card company", 087 "dummy", 088 "Amex", 089 "Diners/Carte Blanche", 090 "Discover", 091 "enRoute", 092 "JCB", 093 "MasterCard", 094 "Visa"}; 095 096 // -------------------------- STATIC METHODS -------------------------- 097 098 static 099 { 100 // now that all enum constants defined 101 buildRanges(); 102 } 103 104 /** 105 * build table of which ranges of credit card number belong to which vendor 106 */ 107 private static void buildRanges() 108 { 109 // careful, no lead zeros allowed 110 // low high len vendor mod-10? 111 ranges = 112 new LCR[] {new LCR( 4000000000000L, 113 4999999999999L, 114 13, 115 VISA, 116 true ), 117 new LCR( 30000000000000L, 118 30599999999999L, 119 14, 120 DINERS, 121 true ), 122 new LCR( 36000000000000L, 123 36999999999999L, 124 14, 125 DINERS, 126 true ), 127 new LCR( 38000000000000L, 128 38999999999999L, 129 14, 130 DINERS, 131 true ), 132 new LCR( 180000000000000L, 133 180099999999999L, 134 15, 135 JCB, 136 true ), 137 new LCR( 201400000000000L, 138 201499999999999L, 139 15, 140 ENROUTE, 141 false ), 142 new LCR( 213100000000000L, 143 213199999999999L, 144 15, 145 JCB, 146 true ), 147 new LCR( 214900000000000L, 148 214999999999999L, 149 15, 150 ENROUTE, 151 false ), 152 new LCR( 340000000000000L, 153 349999999999999L, 154 15, 155 AMEX, 156 true ), 157 new LCR( 370000000000000L, 158 379999999999999L, 159 15, 160 AMEX, 161 true ), 162 new LCR( 3000000000000000L, 163 3999999999999999L, 164 16, 165 JCB, 166 true ), 167 new LCR( 4000000000000000L, 168 4999999999999999L, 169 16, 170 VISA, 171 true ), 172 new LCR( 5100000000000000L, 173 5599999999999999L, 174 16, 175 MASTERCARD, 176 true ), 177 new LCR( 6011000000000000L, 178 6011999999999999L, 179 16, 180 DISCOVER, 181 true )}; // end table initialisation 182 } 183 184 /** 185 * Finds a matching range in the ranges array for a given creditCardNumber. 186 * 187 * @param creditCardNumber number on card. 188 * 189 * @return index of matching range, or NOT_ENOUGH_DIGITS or UNKNOWN_VENDOR 190 * on failure. 191 */ 192 protected static int findMatchingRange( long creditCardNumber ) 193 { 194 if ( creditCardNumber < 1000000000000L ) 195 { 196 return NOT_ENOUGH_DIGITS; 197 } 198 if ( creditCardNumber > 9999999999999999L ) 199 { 200 return TOO_MANY_DIGITS; 201 } 202 // check the cached index first, where we last found a number. 203 if ( ranges[ cachedLastFind ].low <= creditCardNumber 204 && creditCardNumber <= ranges[ cachedLastFind ].high ) 205 { 206 return cachedLastFind; 207 } 208 for ( int i = 0; i < ranges.length; i++ ) 209 { 210 if ( ranges[ i ].low <= creditCardNumber 211 && creditCardNumber <= ranges[ i ].high ) 212 { 213 // we have a match 214 cachedLastFind = i; 215 return i; 216 } 217 } // end for 218 return UNKNOWN_VENDOR; 219 } // end findMatchingRange 220 221 222 public static boolean isValid(String strCreditCardNumber) { 223 return isValid(toLong(strCreditCardNumber, 0L)); 224 } 225 226 private static long toLong(String strCreditCardNumber, long defaultValue) { 227 228 if(strCreditCardNumber==null) return defaultValue; 229 230 // strip commas, spaces, + AND - 231 StringBuffer sb = new StringBuffer( strCreditCardNumber.length() ); 232 for (int i = 0; i < strCreditCardNumber.length(); i++ ) { 233 char c = strCreditCardNumber.charAt( i ); 234 if(!StringUtil.isWhiteSpace(c) && c!=',' && c!='+' && c!='-') 235 sb.append( c ); 236 } 237 long num=(long) Caster.toDoubleValue(sb.toString(),0L); 238 if(num==0L) return defaultValue; 239 240 return num; 241 } 242 243 /** 244 * Determine if the credit card number is valid, i.e. has good prefix and 245 * checkdigit. Does _not_ ask the credit card company if this card has been 246 * issued or is in good standing. 247 * 248 * @param creditCardNumber number on card. 249 * 250 * @return true if card number is good. 251 */ 252 public static boolean isValid( long creditCardNumber ) { 253 int i = findMatchingRange( creditCardNumber ); 254 if ( i < 0 ) { 255 return false; 256 } 257 //else { 258 // we have a match 259 if ( ranges[ i ].hasCheckDigit ) 260 { 261 // there is a checkdigit to be validated 262 /* 263 * Manual method MOD 10 checkdigit 706-511-227 7 0 6 5 1 1 2 2 7 264 * 2 * 2 * 2 * 2 --------------------------------- 7 + 0 + 6 265 * +1+0+ 1 + 2 + 2 + 4 = 23 23 MOD 10 = 3 10 - 3 = 7 -- the 266 * check digit Note digits of multiplication results must be 267 * added before sum. Computer Method MOD 10 checkdigit 268 * 706-511-227 7 0 6 5 1 1 2 2 7 Z Z Z Z 269 * --------------------------------- 7 + 0 + 6 + 1 + 1 + 2 + 2 + 270 * 4 + 7 = 30 30 MOD 10 had better = 0 271 */ 272 long number = creditCardNumber; 273 int checksum = 0; 274 // work right to left 275 for ( int place = 0; place < 16; place++ ) 276 { 277 int digit = (int) ( number % 10 ); 278 number /= 10; 279 if ( ( place & 1 ) == 0 ) 280 { 281 // even position (0-based from right), just add digit 282 checksum += digit; 283 } 284 else 285 { // odd position (0-based from right), must double 286 // and add 287 checksum += z( digit ); 288 } 289 if ( number == 0 ) 290 { 291 break; 292 } 293 } // end for 294 // good checksum should be 0 mod 10 295 return ( checksum % 10 ) == 0; 296 } 297 return true; // no checksum needed 298 299 //} // end if have match 300 } // end isValid 301 302 303 304 /** 305 * Determine the credit card company. Does NOT validate checkdigit. 306 * 307 * @param creditCardNumber number on card. 308 * 309 * @return credit card vendor enumeration constant. 310 */ 311 public static int recognizeVendor( long creditCardNumber ) 312 { 313 int i = findMatchingRange( creditCardNumber ); 314 if ( i < 0 ) 315 { 316 return i; 317 } 318 return ranges[ i ].vendor; 319 320 } // end recognize 321 322 // From http://www.icverify.com/ 323 // Vendor Prefix len checkdigit 324 // MASTERCARD 51-55 16 mod 10 325 // VISA 4 13, 16 mod 10 326 // AMEX 34,37 15 mod 10 327 // Diners Club/ 328 // Carte Blanche 329 // 300-305 14 330 // 36 14 331 // 38 14 mod 10 332 // Discover 6011 16 mod 10 333 // enRoute 2014 15 334 // 2149 15 any 335 // JCB 3 16 mod 10 336 // JCB 2131 15 337 // 1800 15 mod 10 338 339 public static String toCreditcard(String strCreditCardNumber) throws ExpressionException { 340 long number=toLong(strCreditCardNumber, -1); 341 if(number==-1) throw new ExpressionException("invalid creditcard number ["+strCreditCardNumber+"]"); 342 return toPrettyString(number); 343 } 344 public static String toCreditcard(String strCreditCardNumber, String defaultValue) { 345 long number=toLong(strCreditCardNumber, -1); 346 if(number==-1) return defaultValue; 347 return toPrettyString(number); 348 } 349 350 /** 351 * Convert a creditCardNumber as long to a formatted String. Currently it 352 * breaks 16-digit numbers into groups of 4. 353 * 354 * @param creditCardNumber number on card. 355 * 356 * @return String representation of the credit card number. 357 */ 358 private static String toPrettyString( long creditCardNumber ) 359 { 360 String plain = Long.toString( creditCardNumber ); 361 // int i = findMatchingRange(creditCardNumber); 362 int length = plain.length(); 363 364 switch ( length ) 365 { 366 case 12 : 367 // 12 pattern 3-3-3-3 368 return plain.substring( 0, 3 ) 369 + ' ' 370 + plain.substring( 3, 6 ) 371 + ' ' 372 + plain.substring( 6, 9 ) 373 + ' ' 374 + plain.substring( 9, 12 ); 375 376 case 13 : 377 // 13 pattern 4-3-3-3 378 return plain.substring( 0, 4 ) 379 + ' ' 380 + plain.substring( 4, 7 ) 381 + ' ' 382 + plain.substring( 7, 10 ) 383 + ' ' 384 + plain.substring( 10, 13 ); 385 386 case 14 : 387 // 14 pattern 2-4-4-4 388 return plain.substring( 0, 2 ) 389 + ' ' 390 + plain.substring( 2, 6 ) 391 + ' ' 392 + plain.substring( 6, 10 ) 393 + ' ' 394 + plain.substring( 10, 14 ); 395 396 case 15 : 397 // 15 pattern 3-4-4-4 398 return plain.substring( 0, 3 ) 399 + ' ' 400 + plain.substring( 3, 7 ) 401 + ' ' 402 + plain.substring( 7, 11 ) 403 + ' ' 404 + plain.substring( 11, 15 ); 405 406 case 16 : 407 // 16 pattern 4-4-4-4 408 return plain.substring( 0, 4 ) 409 + ' ' 410 + plain.substring( 4, 8 ) 411 + ' ' 412 + plain.substring( 8, 12 ) 413 + ' ' 414 + plain.substring( 12, 16 ); 415 416 case 17 : 417 // 17 pattern 1-4-4-4-4 418 return plain.substring( 0, 1 ) 419 + ' ' 420 + plain.substring( 1, 5 ) 421 + ' ' 422 + plain.substring( 5, 9 ) 423 + ' ' 424 + plain.substring( 9, 13 ) 425 + ' ' 426 + plain.substring( 13, 17 ); 427 428 default : 429 // 0..11, 18+ digits long 430 // plain 431 return plain; 432 } // end switch 433 } // end toPrettyString 434 435 /** 436 * Converts a vendor index enumeration to the equivalent words. It will 437 * trigger an ArrayIndexOutOfBoundsException if you feed it an illegal 438 * value. 439 * 440 * @param vendorEnum e.g. AMEX, UNKNOWN_VENDOR, TOO_MANY_DIGITS 441 * 442 * @return equivalent string in words, e.g. "Amex" "Error: unknown vendor". 443 */ 444 public static String vendorToString( int vendorEnum ) 445 { 446 return vendors[ vendorEnum - NOT_ENOUGH_DIGITS ]; 447 } // end vendorToString 448 449 /** 450 * used in computing checksums, doubles and adds resulting digits. 451 * 452 * @param digit the digit to be doubled, and digit summed. 453 * 454 * @return // 0->0 1->2 2->4 3->6 4->8 5->1 6->3 7->5 8->7 9->9 455 */ 456 private static int z( int digit ) { 457 if ( digit == 0 ) { 458 return 0; 459 } 460 return ( digit * 2 - 1 ) % 9 + 1; 461 } 462 463 // --------------------------- main() method --------------------------- 464 465 /** 466 * Test driver 467 * 468 * @param args not used 469 */ 470 public static void main( String[] args ) 471 { 472 if ( DEBUGGING ) 473 { 474 475 476 477 System.out.println( isValid( "0" ) ); 478 System.out.println( isValid( "4000000000006" ) ); 479 System.out.println( isValid( "40000000-000,06" ) ); 480 481 System.out.println( isValid( 0 ) ); 482 System.out.println( isValid( 6010222233334444L ) ); // false 483 System.out.println( isValid( 4000000000000L ) ); // false 484 System.out.println( isValid( 4000000000006L ) ); // true 485 System.out.println( isValid( 4000000000009L ) ); // false 486 System.out.println( isValid( 4999999999999L ) ); // false 487 System.out.println( isValid( 378888888888858L ) ); // true, Amex 488 System.out.println( isValid( 4888888888888838L ) ); // true, Visa; 489 System.out.println( isValid( 5588888888888838L ) ); // true, MC 490 System.out.println( isValid( 6011222233334444L ) ); // true, Dicover 491 492 } // end if debugging 493 } // end main 494 } // end class CreditCard 495 496 /** 497 * Describes a single Legal Card Range 498 */ 499 class LCR { 500 501 // ------------------------------ FIELDS ------------------------------ 502 503 /** 504 * does this range have a MOD-10 checkdigit? 505 */ 506 public boolean hasCheckDigit; 507 508 /** 509 * low and high bounds on range covered by this vendor 510 */ 511 public long high; 512 513 /** 514 * how many digits in this type of number. 515 */ 516 public int length; 517 518 /** 519 * low bounds on range covered by this vendor 520 */ 521 public long low; 522 523 /** 524 * enumeration credit card service 525 */ 526 public int vendor; 527 528 // --------------------------- CONSTRUCTORS --------------------------- 529 530 /** 531 * public constructor 532 * 533 * @param low lowest credit card number in range. 534 * @param high highest credit card number in range 535 * @param length length of number in digits 536 * @param vendor enum constant for vendor 537 * @param hasCheckDigit true if uses mod 10 check digit. 538 */ 539 public LCR( long low, 540 long high, 541 int length, 542 int vendor, 543 boolean hasCheckDigit ) 544 { 545 this.low = low; 546 this.high = high; 547 this.length = length; 548 this.vendor = vendor; 549 this.hasCheckDigit = hasCheckDigit; 550 } // end public constructor 551 } // end class LCR