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&amp;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}