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.ISOUtil; 022 023import java.util.Objects; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026 027/** 028 * 029 * This class is based on the old 'CardHolder' class and adds support for multiple 030 * PAN and Expiration dates taken from manual entry, track1, track2. It also corrects the name. 031 * 032 * @author apr@jpos.org 033 * @since jPOS 2.0.5 034 * 035 */ 036@SuppressWarnings("unused") 037public class Track2 { 038 private String pan; 039 private String exp; 040 private String cvv; 041 private String serviceCode; 042 private String discretionaryData; 043 private String track; 044 045 private Track2 () { } 046 047 /** 048 * Copies the track2 fields from the supplied {@link Builder}. 049 * 050 * @param builder builder carrying the parsed or assembled track2 fields 051 */ 052 public Track2 (Builder builder) { 053 track = builder.track; 054 pan = builder.pan; 055 exp = builder.exp; 056 cvv = builder.cvv; 057 serviceCode = builder.serviceCode; 058 discretionaryData = builder.discretionaryData; 059 } 060 061 /** 062 * Returns the primary account number. 063 * 064 * @return primary account number 065 */ 066 public String getPan() { 067 return pan; 068 } 069 070 /** 071 * Returns the expiration date. 072 * 073 * @return expiration date in {@code YYMM} form, or {@code null} if absent 074 */ 075 public String getExp() { 076 return exp; 077 } 078 079 /** 080 * Returns the CVV/CVC value, when present. 081 * 082 * @return CVV/CVC value, or {@code null} if not present in the track 083 */ 084 public String getCvv() { 085 return cvv; 086 } 087 088 /** 089 * Returns the service code. 090 * 091 * @return three-digit service code, or {@code null} if absent 092 */ 093 public String getServiceCode() { 094 return serviceCode; 095 } 096 097 /** 098 * Returns the discretionary data trailing the service code. 099 * 100 * @return remaining discretionary data trailing the service code, or {@code null} 101 */ 102 public String getDiscretionaryData() { 103 return discretionaryData; 104 } 105 106 /** 107 * Returns the raw track2 string this object was built from. 108 * 109 * @return raw track2 string this object was built from, or {@code null} when assembled programmatically 110 */ 111 public String getTrack() { 112 return track; 113 } 114 115 @Override 116 public String toString() { 117 return pan != null ? ISOUtil.protect(pan) : "nil"; 118 } 119 120 @Override 121 public boolean equals(Object o) { 122 if (this == o) return true; 123 if (o == null || getClass() != o.getClass()) return false; 124 Track2 track21 = (Track2) o; 125 return Objects.equals(pan, track21.pan) && 126 Objects.equals(exp, track21.exp) && 127 Objects.equals(cvv, track21.cvv) && 128 Objects.equals(serviceCode, track21.serviceCode) && 129 Objects.equals(discretionaryData, track21.discretionaryData) && 130 Objects.equals(track, track21.track); 131 } 132 133 @Override 134 public int hashCode() { 135 return Objects.hash(pan, exp, cvv, serviceCode, discretionaryData, track); 136 } 137 138 /** 139 * Creates a new builder for assembling a {@code Track2}. 140 * 141 * @return a new {@link Builder} for assembling a {@code Track2} 142 */ 143 public static Builder builder() { 144 return new Builder(); 145 } 146 147 /** 148 * Fluent builder that parses a raw track2 string or assembles a {@code Track2} 149 * from individual fields and validates the result against a configurable pattern. 150 */ 151 public static class Builder { 152 private static String TRACK2_EXPR = "^([0-9]{1,19})[=D]([0-9]{4})?([0-9]{3})?([0-9]{4})?([0-9]{1,})?$"; 153 private static Pattern TRACK2_PATTERN = Pattern.compile(TRACK2_EXPR); 154 private String pan; 155 private String exp; 156 private String cvv; 157 private String serviceCode; 158 private String discretionaryData; 159 private String track; 160 private Pattern pattern = TRACK2_PATTERN; 161 162 private Builder () { } 163 164 /** 165 * Sets the primary account number. 166 * 167 * @param pan primary account number 168 * @return this builder 169 */ 170 public Builder pan (String pan) { 171 this.pan = pan; return this; 172 } 173 174 /** 175 * Sets the expiration date. 176 * 177 * @param exp expiration date in {@code YYMM} form 178 * @return this builder 179 */ 180 public Builder exp (String exp) { 181 this.exp = exp; return this; 182 } 183 184 /** 185 * Sets the CVV/CVC value. 186 * 187 * @param cvv CVV/CVC value 188 * @return this builder 189 */ 190 public Builder cvv (String cvv) { 191 this.cvv = cvv; return this; 192 } 193 194 /** 195 * Sets the service code. 196 * 197 * @param serviceCode three-digit service code 198 * @return this builder 199 */ 200 public Builder serviceCode (String serviceCode) { 201 this.serviceCode = serviceCode; return this; 202 } 203 204 /** 205 * Sets the discretionary data trailing the service code. 206 * 207 * @param discretionaryData discretionary data trailing the service code 208 * @return this builder 209 */ 210 public Builder discretionaryData (String discretionaryData) { 211 this.discretionaryData = discretionaryData; 212 return this; 213 } 214 215 /** 216 * Optional method, can be used to override default pattern 217 * @param pattern overrides default pattern 218 * @return this builder 219 */ 220 public Builder pattern (Pattern pattern) { 221 this.pattern = pattern; 222 return this; 223 } 224 225 /** 226 * Parses a raw track2 string and populates the builder fields. 227 * 228 * @param s raw track2 data 229 * @return this builder 230 * @throws InvalidCardException if {@code s} is null, exceeds 37 characters, 231 * or does not match the configured pattern 232 */ 233 public Builder track (String s) 234 throws InvalidCardException 235 { 236 if (s == null) 237 throw new InvalidCardException ("null track2 data"); 238 if (s.length() > 37) 239 throw new InvalidCardException("track2 too long"); 240 241 track = s; 242 Matcher matcher = pattern.matcher(s); 243 int cnt = matcher.groupCount(); 244 if (matcher.find() && cnt >= 1) { 245 pan = matcher.group(1); 246 if (cnt > 1) 247 exp = matcher.group(2); 248 if (cnt > 2) 249 serviceCode = matcher.group(3); 250 if (cnt > 3) 251 cvv = matcher.group(4); 252 if (cnt > 4) 253 discretionaryData = matcher.group(5); 254 } else { 255 throw new InvalidCardException ("invalid track2"); 256 } 257 return this; 258 } 259 260 /** 261 * Constructs the Track2 data based on the card data provided. 262 * The generated Track2 data is validated using the pattern. 263 * If the Track2 data doesn't match the pattern, the track attribute keeps the original value. 264 * @return this builder. 265 */ 266 public Builder buildTrackData() { 267 StringBuilder track2 = new StringBuilder(this.pan); 268 track2.append("="); 269 track2.append(this.exp); 270 track2.append(this.serviceCode); 271 track2.append(this.cvv); 272 track2.append(this.discretionaryData); 273 274 Matcher matcher = this.pattern.matcher(track2); 275 int cnt = matcher.groupCount(); 276 if (matcher.find() && cnt >= 1) 277 this.track = track2.toString(); 278 279 return this; 280 } 281 282 /** 283 * Builds the immutable {@link Track2}. If no raw track string was set, 284 * one is assembled from the individual fields via {@link #buildTrackData()}. 285 * 286 * @return the built {@link Track2} 287 */ 288 public Track2 build() { 289 if (this.track == null) 290 buildTrackData(); 291 return new Track2(this); 292 } 293 } 294}