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 * @author apr@cs.com.uy 033 * @since jPOS 1.1 034 * 035 * This class is called 'CardHolder', but a better name could have been 'Card' 036 * At some point we'll deprecate this one and create a new 'Card' class. 037 */ 038public class CardHolder implements Cloneable, Serializable, Loggeable { 039 private static final long serialVersionUID = 7449770625551878435L; 040 private static final String TRACK1_SEPARATOR = "^"; 041 private static final char TRACK2_SEPARATOR = '='; 042 private static final int BINLEN = 6; 043 private static final int MINPANLEN = 10; 044 /** 045 * Primary Account Number 046 * @serial 047 */ 048 protected String pan; 049 /** 050 * Expiration date (YYMM) 051 * @serial 052 */ 053 protected String exp; 054 /** 055 * Track2 trailler 056 * @serial 057 */ 058 protected String trailer; 059 /** 060 * Optional security code (CVC, CVV, Locale ID, wse) 061 * @serial 062 */ 063 protected String securityCode; 064 /** 065 * Track1 Data 066 * @serial 067 */ 068 protected String track1; 069 070 /** 071 * creates an empty CardHolder 072 */ 073 public CardHolder() { 074 super(); 075 } 076 077 /** 078 * creates a new CardHolder based on track2 079 * @param track2 cards track2 080 * @exception InvalidCardException 081 */ 082 public CardHolder (String track2) 083 throws InvalidCardException 084 { 085 super(); 086 parseTrack2 (track2); 087 } 088 089 /** 090 * creates a new CardHolder based on pan and exp 091 * @exception InvalidCardException 092 */ 093 public CardHolder (String pan, String exp) 094 throws InvalidCardException 095 { 096 super(); 097 setPAN (pan); 098 setEXP (exp); 099 } 100 101 /** 102 * Construct a CardHolder based on content received on 103 * field 35 (track2) or field 2 (PAN) + field 14 (EXP) 104 * @param m an ISOMsg 105 * @throws InvalidCardException 106 */ 107 public CardHolder (ISOMsg m) 108 throws InvalidCardException 109 { 110 super(); 111 if (m.hasField(35)) 112 parseTrack2((String) m.getValue(35)); 113 else if (m.hasField(2)) { 114 setPAN((String) m.getValue(2)); 115 if (m.hasField(14)) 116 setEXP((String) m.getValue(14)); 117 } else { 118 throw new InvalidCardException("required fields not present"); 119 } 120 if (m.hasField(45)) { 121 setTrack1((String) m.getValue(45)); 122 } 123 if (m.hasField(55)) { 124 setSecurityCode(m.getString(55)); 125 } 126 } 127 128 /** 129 * extract pan/exp/trailler from track2 130 * @param s a valid track2 131 * @exception InvalidCardException 132 */ 133 public void parseTrack2 (String s) 134 throws InvalidCardException 135 { 136 if (s == null) 137 throw new InvalidCardException ("null track2 data"); 138 int separatorIndex = s.replace ('D','=').indexOf(TRACK2_SEPARATOR); 139 if (separatorIndex > 0 && s.length() > separatorIndex+4) { 140 pan = s.substring(0, separatorIndex); 141 exp = s.substring(separatorIndex+1, separatorIndex+1+4); 142 trailer = s.substring(separatorIndex+1+4); 143 } else 144 throw new InvalidCardException ("Invalid track2 format"); 145 } 146 147 /** 148 * @param track1 card's track1 149 */ 150 public void setTrack1(String track1) { 151 this.track1 = track1; 152 } 153 154 /** 155 * @return the track1 156 */ 157 public String getTrack1() { 158 return track1; 159 } 160 161 /** 162 * @return true if we have a track1 163 */ 164 public boolean hasTrack1() { 165 return track1!=null; 166 } 167 168 /** 169 * @return the Name written on the card (from track1) 170 */ 171 public String getNameOnCard() { 172 String name = null; 173 if (track1!=null) { 174 StringTokenizer st = 175 new StringTokenizer(track1, TRACK1_SEPARATOR); 176 if (st.countTokens()<2) 177 return null; 178 st.nextToken(); // Skips the first token 179 name = st.nextToken(); // This is the name 180 } 181 return name; 182 } 183 184 /** 185 * @return reconstructed track2 or null 186 */ 187 public String getTrack2() { 188 if (hasTrack2()) 189 return pan + TRACK2_SEPARATOR + exp + trailer; 190 else 191 return null; 192 } 193 /** 194 * @return true if we have a (may be valid) track2 195 */ 196 public boolean hasTrack2() { 197 return pan != null && exp != null && trailer != null; 198 } 199 200 /** 201 * assigns securityCode to this CardHolder object 202 * @param securityCode Card's security code 203 */ 204 public void setSecurityCode(String securityCode) { 205 this.securityCode = securityCode; 206 } 207 /** 208 * @return securityCode (or null) 209 */ 210 public String getSecurityCode() { 211 return securityCode; 212 } 213 /** 214 * @return true if we have a security code 215 */ 216 public boolean hasSecurityCode() { 217 return securityCode != null; 218 } 219 /** 220 * @deprecated use getTrailer() 221 * @return trailer (may be null) 222 */ 223 @SuppressWarnings("unused") 224 @Deprecated 225 public String getTrailler() { 226 return trailer; 227 } 228 /** 229 * Set Card's trailer 230 * @deprecated use setTrailer 231 * @param trailer Card's trailer 232 */ 233 @SuppressWarnings("unused") 234 @Deprecated 235 public void setTrailler (String trailer) { 236 this.trailer = trailer; 237 } 238 239 public String getTrailer() { 240 return trailer; 241 } 242 243 public void setTrailer(String trailer) { 244 this.trailer = trailer; 245 } 246 247 /** 248 * Sets Primary Account Number 249 * @param pan Primary Account NUmber 250 * @exception InvalidCardException 251 */ 252 public void setPAN (String pan) 253 throws InvalidCardException 254 { 255 if (pan.length() < MINPANLEN) 256 throw new InvalidCardException ("PAN length smaller than min required"); 257 this.pan = pan; 258 } 259 260 /** 261 * @return Primary Account Number 262 */ 263 public String getPAN () { 264 return pan; 265 } 266 267 268 /** 269 * Get the first <code>len</code> digits from the PAN. 270 * Can be used for the newer 8-digit BINs, or some arbitrary length. 271 * @return <code>len</code>-digit bank issuer number 272 */ 273 public String getBIN (int len) { 274 return pan.substring(0, len); 275 } 276 277 /** 278 * Get the traditional 6-digit BIN (Bank Issuer Number) from the PAN 279 * @return 6-digit bank issuer number 280 */ 281 public String getBIN () { 282 return getBIN(BINLEN); 283 } 284 285 /** 286 * Set Expiration Date 287 * @param exp card expiration date 288 * @exception InvalidCardException 289 */ 290 public void setEXP (String exp) 291 throws InvalidCardException 292 { 293 if (exp.length() != 4) 294 throw new InvalidCardException ("Invalid Exp length, must be 4"); 295 this.exp = exp; 296 } 297 298 /** 299 * Get Expiration Date 300 * @return card expiration date 301 */ 302 public String getEXP () { 303 return exp; 304 } 305 306 /** 307 * Y2K compliant expiration check 308 * @return true if card is expired (or expiration is invalid) 309 */ 310 public boolean isExpired () { 311 return isExpired(new Date()); 312 } 313 314 /** 315 * Y2K compliant expiration check 316 * @param currentDate current system's date 317 * @return true if card is expired (or expiration is invalid) 318 */ 319 public boolean isExpired(Date currentDate) { 320 if (exp == null || exp.length() != 4) 321 return true; 322 String now = ISODate.formatDate(currentDate, "yyyyMM"); 323 try { 324 int mm = Integer.parseInt(exp.substring(2)); 325 int aa = Integer.parseInt(exp.substring(0,2)); 326 if (aa < 100 && mm > 0 && mm <= 12) { 327 String expDate = (aa < 70 ? "20" : "19") + exp; 328 if (expDate.compareTo(now) >= 0) 329 return false; 330 } 331 } catch (NumberFormatException ignored) { 332 // NOPMD 333 } 334 return true; 335 } 336 public boolean isValidCRC () { 337 return isValidCRC(this.pan); 338 } 339 public static boolean isValidCRC (String p) { 340 int i, crc; 341 342 int odd = p.length() % 2; 343 344 for (i=crc=0; i<p.length(); i++) { 345 char c = p.charAt(i); 346 if (!Character.isDigit (c)) 347 return false; 348 c = (char) (c - '0'); 349 if (i % 2 == odd) 350 crc+= c*2 >= 10 ? c*2 -9 : c*2; 351 else 352 crc+=c; 353 } 354 return crc % 10 == 0; 355 } 356 357 /** 358 * dumps CardHolder basic information<br> 359 * by default we do not dump neither track1/2 nor securityCode 360 * for security reasons. 361 * @param p a PrintStream usually suplied by Logger 362 * @param indent ditto 363 * @see org.jpos.util.Loggeable 364 */ 365 public void dump (PrintStream p, String indent) { 366 p.print (indent + "<CardHolder"); 367 if (hasTrack1()) 368 p.print (" trk1=\"true\""); 369 370 if (hasTrack2()) 371 p.print (" trk2=\"true\""); 372 373 if (hasSecurityCode()) 374 p.print (" sec=\"true\""); 375 376 if (isExpired()) 377 p.print (" expired=\"true\""); 378 379 p.println (">"); 380 p.println (indent + " " + "<pan>" +pan +"</pan>"); 381 p.println (indent + " " + "<exp>" +exp +"</exp>"); 382 p.println (indent + "</CardHolder>"); 383 } 384 385 /** 386 * @return ServiceCode (if available) or a String with three blanks 387 */ 388 public String getServiceCode () { 389 return trailer != null && trailer.length() >= 3 ? 390 trailer.substring (0, 3) : 391 " "; 392 } 393 public boolean seemsManualEntry() { 394 return trailer == null || trailer.trim().length() == 0; 395 } 396 397 @Override 398 public int hashCode() { 399 final int prime = 31; 400 int result = 1; 401 result = prime * result + (exp == null ? 0 : exp.hashCode()); 402 result = prime * result + (pan == null ? 0 : pan.hashCode()); 403 return result; 404 } 405 406 @Override 407 public boolean equals(Object obj) { 408 if (this == obj) 409 return true; 410 if (obj == null) 411 return false; 412 if (getClass() != obj.getClass()) 413 return false; 414 CardHolder other = (CardHolder) obj; 415 if (exp == null) { 416 if (other.exp != null) 417 return false; 418 } else if (!exp.equals(other.exp)) 419 return false; 420 if (pan == null) { 421 if (other.pan != null) 422 return false; 423 } else if (!pan.equals(other.pan)) 424 return false; 425 return true; 426 } 427}