/* Copyright 2007 Aaron Porter * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package net.sourceforge.stripes.validation; import java.util.Collection; import java.util.Locale; /** * <p>A faux TypeConverter that validates that the String supplied is a valid credit card number. * The Luhn algorithm is used in addition to valid credit card prefixes to determine if the string * being converted qualifies as a credit card number. This DOES NOT check to see if an actual card * with the specified number exists, only that it appears to be a valid card number.</p> * * <p>If the credit card number is not valid a single error message will be generated. The error * message is a scoped message with a default scope of <tt>converter.creditCard</tt> and name * <tt>invalidCreditCard</tt>. As a result error messages will be looked for in the following * order:</p> * * <ul> * <li>beanClassFQN.fieldName.invalidCreditCard</li> * <li>actionPath.fieldName.invalidCreditCard</li> * <li>fieldName.invalidCreditCard</li> * <li>beanClassFQN.invalidCreditCard</li> * <li>actionPath.invalidCreditCard</li> * <li>converter.creditCard.invalidCreditCard</li> * </ul> * * @author Aaron Porter * @since Stripes 1.5 */ public class CreditCardTypeConverter implements TypeConverter<String> { // Recognized card types public enum Type { AMEX,DinersClub,Discover,enRoute,JCB,MasterCard,VISA } /** Accepts the Locale provided, but does nothing with it since credit card numbers are Locale-less. */ public void setLocale(Locale locale) { /** Doesn't matter for credit cards. */ } /** * Validates the user input to ensure that it looks like a valid credit card number. * * @param input the String input, always a non-null non-empty String * @param targetType realistically always String since java.lang.String is final * @param errors a non-null collection of errors to populate in case of error * @return the credit card with any non-numeric characters removed or null */ public String convert(String input, Class<? extends String> targetType, Collection<ValidationError> errors) { // Remove any non-numeric characters String cardNumber = input.replaceAll("\\D", ""); if (getCardType(cardNumber) != null) return cardNumber; errors.add( new ScopedLocalizableError("converter.creditCard", "invalidCreditCard") ); return null; } /** * Determines the type of card from the card number passed in. * * @param cardNumber the card number to check * @return the type of card or null if the card number is invalid for all known card types */ public static Type getCardType(String cardNumber) { if (!isLuhnValid(cardNumber)) return null; if (checkCard(cardNumber, 15, "34", "37")) return Type.AMEX; if (checkCard(cardNumber, 14, "30", "36", "38")) return Type.DinersClub; if (checkCard(cardNumber, 16, "6011")) return Type.Discover; if (checkCard(cardNumber, 15, "2014", "2149")) return Type.enRoute; if (checkCard(cardNumber, 16, "3088","3096","3112","3158","3337","3528")) return Type.JCB; if (checkCard(cardNumber, 16, "51", "52", "53", "54", "55")) return Type.MasterCard; if (checkCard(cardNumber, 13, "4") || checkCard(cardNumber, 16, "4")) return Type.VISA; return null; } /** * Checks cardNumber to see if it is the correct length and starts with * one of the specified prefixes. * * @param cardNumber the card number to check * @param length the correct string length * @param prefixes possible prefixes * @return true when all conditions are met */ private static boolean checkCard(String cardNumber, int length, String...prefixes) { if (cardNumber.length() != length) return false; for (String prefix : prefixes) if (cardNumber.startsWith(prefix)) return true; return false; } /** * Performs the Luhn algorithm on the card number to determine if it is valid * as a credit card number. * * @param cardNumber the card number to check * @return true if cardNumber looks like a valid credit card number */ private static boolean isLuhnValid(String cardNumber) { if (cardNumber.length() < 13 || cardNumber.length() > 16) return false; int sum = 0; for (int i = 0, length = cardNumber.length(); i < length; i++) { int pos = length - i - 1; int v = Integer.parseInt(cardNumber.substring(pos, pos + 1)); if (i % 2 == 1) v *= 2; sum += v / 10 + v % 10; } return sum % 10 == 0; } }