001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.core; 020 021import org.jpos.iso.ISODate; 022import org.jpos.iso.ISOException; 023import org.jpos.iso.ISOMsg; 024import org.jpos.util.Loggeable; 025 026import java.io.PrintStream; 027import java.io.Serializable; 028import java.util.Date; 029import java.util.StringTokenizer; 030 031/** 032 * 033 * This class is called 'CardHolder', but a better name could have been 'Card' 034 * At some point we'll deprecate this one and create a new 'Card' class. 035 * 036 * @author apr@cs.com.uy 037 * @since jPOS 1.1 038 * 039 */ 040public class CardHolder implements Cloneable, Serializable, Loggeable { 041 private static final long serialVersionUID = 7449770625551878435L; 042 private static final String TRACK1_SEPARATOR = "^"; 043 private static final char TRACK2_SEPARATOR = '='; 044 private static final int BINLEN = 6; 045 private static final int MINPANLEN = 10; 046 /** 047 * Primary Account Number 048 * @serial 049 */ 050 protected String pan; 051 /** 052 * Expiration date (YYMM) 053 * @serial 054 */ 055 protected String exp; 056 /** 057 * Track2 trailler 058 * @serial 059 */ 060 protected String trailer; 061 /** 062 * Optional security code (CVC, CVV, Locale ID, wse) 063 * @serial 064 */ 065 protected String securityCode; 066 /** 067 * Track1 Data 068 * @serial 069 */ 070 protected String track1; 071 072 /** 073 * creates an empty CardHolder 074 */ 075 public CardHolder() { 076 super(); 077 } 078 079 /** 080 * creates a new CardHolder based on track2 081 * @param track2 cards track2 082 * @exception InvalidCardException if card data fails validation 083 */ 084 public CardHolder (String track2) 085 throws InvalidCardException 086 { 087 super(); 088 parseTrack2 (track2); 089 } 090 091 /** 092 * Creates a new CardHolder with the given PAN and expiry date. 093 * @param pan the primary account number 094 * @param exp the expiry date (YYMM) 095 * @exception InvalidCardException if card data is invalid 096 */ 097 public CardHolder (String pan, String exp) 098 throws InvalidCardException 099 { 100 super(); 101 setPAN (pan); 102 setEXP (exp); 103 } 104 105 /** 106 * Construct a CardHolder based on content received on 107 * field 35 (track2) or field 2 (PAN) + field 14 (EXP) 108 * @param m an ISOMsg 109 * @throws InvalidCardException if card data is invalid 110 */ 111 public CardHolder (ISOMsg m) 112 throws InvalidCardException 113 { 114 super(); 115 if (m.hasField(35)) 116 parseTrack2((String) m.getValue(35)); 117 else if (m.hasField(2)) { 118 setPAN((String) m.getValue(2)); 119 if (m.hasField(14)) 120 setEXP((String) m.getValue(14)); 121 } else { 122 throw new InvalidCardException("required fields not present"); 123 } 124 if (m.hasField(45)) { 125 setTrack1((String) m.getValue(45)); 126 } 127 if (m.hasField(55)) { 128 setSecurityCode(m.getString(55)); 129 } 130 } 131 132 /** 133 * extract pan/exp/trailler from track2 134 * @param s a valid track2 135 * @exception InvalidCardException if card data is invalid 136 */ 137 public void parseTrack2 (String s) 138 throws InvalidCardException 139 { 140 if (s == null) 141 throw new InvalidCardException ("null track2 data"); 142 int separatorIndex = s.replace ('D','=').indexOf(TRACK2_SEPARATOR); 143 if (separatorIndex > 0 && s.length() > separatorIndex+4) { 144 pan = s.substring(0, separatorIndex); 145 exp = s.substring(separatorIndex+1, separatorIndex+1+4); 146 trailer = s.substring(separatorIndex+1+4); 147 } else 148 throw new InvalidCardException ("Invalid track2 format"); 149 } 150 151 /** 152 * Sets the track1 data. 153 * @param track1 card's track1 154 */ 155 public void setTrack1(String track1) { 156 this.track1 = track1; 157 } 158 159 /** 160 * Returns the track 1 raw data. 161 * @return the track1 string, or null 162 */ 163 public String getTrack1() { 164 return track1; 165 } 166 167 /** 168 * Returns true if track1 data is present. 169 * @return true if we have a track1 170 */ 171 public boolean hasTrack1() { 172 return track1!=null; 173 } 174 175 /** 176 * Returns the cardholder name from track1. 177 * @return the Name written on the card (from track1) 178 */ 179 public String getNameOnCard() { 180 String name = null; 181 if (track1!=null) { 182 StringTokenizer st = 183 new StringTokenizer(track1, TRACK1_SEPARATOR); 184 if (st.countTokens()<2) 185 return null; 186 st.nextToken(); // Skips the first token 187 name = st.nextToken(); // This is the name 188 } 189 return name; 190 } 191 192 /** 193 * Returns a reconstructed track 2 string, or null if track 2 data is absent. 194 * @return reconstructed track2 or null 195 */ 196 public String getTrack2() { 197 if (hasTrack2()) 198 return pan + TRACK2_SEPARATOR + exp + trailer; 199 else 200 return null; 201 } 202 /** 203 * Returns true if track2 data is (potentially) present. 204 * @return true if we have a (may be valid) track2 205 */ 206 public boolean hasTrack2() { 207 return pan != null && exp != null && trailer != null; 208 } 209 210 /** 211 * assigns securityCode to this CardHolder object 212 * @param securityCode Card's security code 213 */ 214 public void setSecurityCode(String securityCode) { 215 this.securityCode = securityCode; 216 } 217 /** 218 * Returns the card security code (CVV/CVC), or null. 219 * @return securityCode (or null) 220 */ 221 public String getSecurityCode() { 222 return securityCode; 223 } 224 /** 225 * Returns true if a security code is present. 226 * @return true if we have a security code 227 */ 228 public boolean hasSecurityCode() { 229 return securityCode != null; 230 } 231 /** 232 * @deprecated use getTrailer() 233 * @return trailer (may be null) 234 */ 235 @SuppressWarnings("unused") 236 @Deprecated 237 public String getTrailler() { 238 return trailer; 239 } 240 /** 241 * Set Card's trailer 242 * @deprecated use setTrailer 243 * @param trailer Card's trailer 244 */ 245 @SuppressWarnings("unused") 246 @Deprecated 247 public void setTrailler (String trailer) { 248 this.trailer = trailer; 249 } 250 251 /** 252 * Returns the card trailer string. 253 * @return card trailer 254 */ 255 public String getTrailer() { 256 return trailer; 257 } 258 259 /** 260 * Sets the card trailer string. 261 * @param trailer card trailer 262 */ 263 public void setTrailer(String trailer) { 264 this.trailer = trailer; 265 } 266 267 /** 268 * Sets Primary Account Number 269 * @param pan Primary Account NUmber 270 * @exception InvalidCardException if the PAN is too short or fails validation 271 */ 272 public void setPAN (String pan) 273 throws InvalidCardException 274 { 275 if (pan.length() < MINPANLEN) 276 throw new InvalidCardException ("PAN length smaller than min required"); 277 this.pan = pan; 278 } 279 280 /** 281 * Returns the Primary Account Number. 282 * @return Primary Account Number 283 */ 284 public String getPAN () { 285 return pan; 286 } 287 288 289 /** 290 * Get the first <code>len</code> digits from the PAN. 291 * Can be used for the newer 8-digit BINs, or some arbitrary length. 292 * @return <code>len</code>-digit bank issuer number 293 */ 294 /** 295 * Returns the first {@code len} digits of the PAN (the BIN). 296 * @param len number of BIN digits to return 297 * @return the BIN prefix 298 */ 299 public String getBIN (int len) { 300 return pan.substring(0, len); 301 } 302 303 /** 304 * Get the traditional 6-digit BIN (Bank Issuer Number) from the PAN 305 * @return 6-digit bank issuer number 306 */ 307 public String getBIN () { 308 return getBIN(BINLEN); 309 } 310 311 /** 312 * Set Expiration Date 313 * @param exp card expiration date 314 * @exception InvalidCardException if card data is invalid 315 */ 316 public void setEXP (String exp) 317 throws InvalidCardException 318 { 319 if (exp.length() != 4) 320 throw new InvalidCardException ("Invalid Exp length, must be 4"); 321 this.exp = exp; 322 } 323 324 /** 325 * Get Expiration Date 326 * @return card expiration date 327 */ 328 public String getEXP () { 329 return exp; 330 } 331 332 /** 333 * Y2K compliant expiration check 334 * @return true if card is expired (or expiration is invalid) 335 */ 336 public boolean isExpired () { 337 return isExpired(new Date()); 338 } 339 340 /** 341 * Y2K compliant expiration check 342 * @param currentDate current system's date 343 * @return true if card is expired (or expiration is invalid) 344 */ 345 public boolean isExpired(Date currentDate) { 346 if (exp == null || exp.length() != 4) 347 return true; 348 String now = ISODate.formatDate(currentDate, "yyyyMM"); 349 try { 350 int mm = Integer.parseInt(exp.substring(2)); 351 int aa = Integer.parseInt(exp.substring(0,2)); 352 if (aa < 100 && mm > 0 && mm <= 12) { 353 String expDate = (aa < 70 ? "20" : "19") + exp; 354 if (expDate.compareTo(now) >= 0) 355 return false; 356 } 357 } catch (NumberFormatException ignored) { 358 // NOPMD 359 } 360 return true; 361 } 362 /** 363 * Returns true if the PAN passes the Luhn (mod-10) check. 364 * @return true if the Luhn check passes 365 */ 366 public boolean isValidCRC () { 367 return isValidCRC(this.pan); 368 } 369 /** 370 * Returns true if the given PAN passes the Luhn (mod-10) check. 371 * @param p the PAN to validate 372 * @return true if the Luhn check passes 373 */ 374 public static boolean isValidCRC (String p) { 375 int i, crc; 376 377 int odd = p.length() % 2; 378 379 for (i=crc=0; i<p.length(); i++) { 380 char c = p.charAt(i); 381 if (!Character.isDigit (c)) 382 return false; 383 c = (char) (c - '0'); 384 if (i % 2 == odd) 385 crc+= c*2 >= 10 ? c*2 -9 : c*2; 386 else 387 crc+=c; 388 } 389 return crc % 10 == 0; 390 } 391 392 /** 393 * dumps CardHolder basic information<br> 394 * by default we do not dump neither track1/2 nor securityCode 395 * for security reasons. 396 * @param p a PrintStream usually suplied by Logger 397 * @param indent ditto 398 * @see org.jpos.util.Loggeable 399 */ 400 public void dump (PrintStream p, String indent) { 401 p.print (indent + "<CardHolder"); 402 if (hasTrack1()) 403 p.print (" trk1=\"true\""); 404 405 if (hasTrack2()) 406 p.print (" trk2=\"true\""); 407 408 if (hasSecurityCode()) 409 p.print (" sec=\"true\""); 410 411 if (isExpired()) 412 p.print (" expired=\"true\""); 413 414 p.println (">"); 415 p.println (indent + " " + "<pan>" +pan +"</pan>"); 416 p.println (indent + " " + "<exp>" +exp +"</exp>"); 417 p.println (indent + "</CardHolder>"); 418 } 419 420 /** 421 * Returns the service code from track2, or three blanks if not available. 422 * @return ServiceCode (if available) or a String with three blanks 423 */ 424 public String getServiceCode () { 425 return trailer != null && trailer.length() >= 3 ? 426 trailer.substring (0, 3) : 427 " "; 428 } 429 /** 430 * Returns true if this card appears to have been entered manually. 431 * @return true if manual entry is suspected 432 */ 433 public boolean seemsManualEntry() { 434 return trailer == null || trailer.trim().length() == 0; 435 } 436 437 @Override 438 public int hashCode() { 439 final int prime = 31; 440 int result = 1; 441 result = prime * result + (exp == null ? 0 : exp.hashCode()); 442 result = prime * result + (pan == null ? 0 : pan.hashCode()); 443 return result; 444 } 445 446 @Override 447 public boolean equals(Object obj) { 448 if (this == obj) 449 return true; 450 if (obj == null) 451 return false; 452 if (getClass() != obj.getClass()) 453 return false; 454 CardHolder other = (CardHolder) obj; 455 if (exp == null) { 456 if (other.exp != null) 457 return false; 458 } else if (!exp.equals(other.exp)) 459 return false; 460 if (pan == null) { 461 if (other.pan != null) 462 return false; 463 } else if (!pan.equals(other.pan)) 464 return false; 465 return true; 466 } 467}