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.transaction.participant;
020
021import java.util.Arrays;
022import java.util.Set;
023import java.util.HashSet;
024import java.util.StringTokenizer;
025import java.io.Serializable;
026import java.util.regex.Pattern;
027
028import org.jpos.core.*;
029import org.jpos.iso.*;
030import org.jpos.rc.CMF;
031import org.jpos.rc.Result;
032import org.jpos.transaction.Context;
033import org.jpos.transaction.ContextConstants;
034import org.jpos.transaction.TransactionParticipant;
035import org.jpos.util.Caller;
036
037import static org.jpos.transaction.ContextConstants.*;
038
039public class CheckFields implements TransactionParticipant, Configurable {
040    private Configuration cfg;
041    private String request;
042    private Pattern PCODE_PATTERN = Pattern.compile("^[\\d|\\w]{6}$");
043    private Pattern TID_PATTERN = Pattern.compile("^[\\w\\s]{1,16}");
044    private Pattern MID_PATTERN = Pattern.compile("^[\\w\\s]{1,15}");
045    private Pattern TRANSMISSION_TIMESTAMP_PATTERN = Pattern.compile("^\\d{10}");
046    private Pattern LOCAL_TIMESTAMP_PATTERN = Pattern.compile("^\\d{14}");
047    private Pattern CAPTUREDATE_PATTERN = Pattern.compile("^\\d{4}");
048    private Pattern ORIGINAL_DATA_ELEMENTS_PATTERN = Pattern.compile("^\\d{30,41}$");
049    private boolean ignoreCardValidation = false;
050    private boolean allowExtraFields = false;
051    private Pattern track1Pattern = null;
052    private Pattern track2Pattern = null;
053
054    public int prepare (long id, Serializable context) {
055        Context ctx = (Context) context;
056        Result rc = ctx.getResult();
057        try {
058            ISOMsg m = ctx.get (request);
059            if (m == null) {
060                ctx.getResult().fail(CMF.INVALID_TRANSACTION, Caller.info(), "'%s' not available in Context", request);
061                return ABORTED | NO_JOIN | READONLY;
062            }
063            Set<String> validFields = new HashSet<>();
064            assertFields (ctx, m, cfg.get ("mandatory", ""), true, validFields, rc);
065            assertFields (ctx, m, cfg.get ("optional", ""), false, validFields, rc);
066            if (!allowExtraFields) assertNoExtraFields (m, validFields, rc);
067        } catch (Throwable t) {
068            rc.fail(CMF.SYSTEM_ERROR, Caller.info(), t.getMessage());
069            ctx.log(t);
070        }
071        return (rc.hasFailures() ? ABORTED : PREPARED) | NO_JOIN | READONLY;
072    }
073
074    public void setConfiguration (Configuration cfg) {
075        this.cfg = cfg;
076        request = cfg.get ("request", ContextConstants.REQUEST.toString());
077        ignoreCardValidation = cfg.getBoolean("ignore-card-validation", false);
078        allowExtraFields = cfg.getBoolean("allow-extra-fields", false);
079        String t1 = cfg.get("track1-pattern", null);
080        if (t1 != null) {
081            track1Pattern = Pattern.compile(t1);
082        }
083        String t2 = cfg.get("track2-pattern", null);
084        if (t2 != null) {
085            track2Pattern = Pattern.compile(t2);
086        }
087    }
088
089    private void assertFields(Context ctx, ISOMsg m, String fields, boolean mandatory, Set<String> validFields, Result rc) {
090        StringTokenizer st = new StringTokenizer (fields, ", ");
091        while (st.hasMoreTokens()) {
092            String s = st.nextToken();
093            ContextConstants k = null;
094            try {
095                k = ContextConstants.valueOf(s);
096            } catch (IllegalArgumentException ignored) { }
097            if (k != null) {
098                switch (k) {
099                    case PCODE:
100                        putPCode(ctx, m, mandatory, validFields, rc);
101                        break;
102                    case CARD:
103                        putCard(ctx, m, mandatory, validFields, rc);
104                        break;
105                    case TID:
106                        putTid(ctx, m, mandatory, validFields, rc);
107                        break;
108                    case MID:
109                        putMid(ctx, m, mandatory, validFields, rc);
110                        break;
111                    case TRANSMISSION_TIMESTAMP:
112                        putTimestamp(ctx, m, TRANSMISSION_TIMESTAMP.toString(), 7, TRANSMISSION_TIMESTAMP_PATTERN, mandatory, validFields, rc);
113                        break;
114                    case TRANSACTION_TIMESTAMP:
115                        putTimestamp(ctx, m, TRANSACTION_TIMESTAMP.toString(), 12, LOCAL_TIMESTAMP_PATTERN, mandatory, validFields, rc);
116                        break;
117                    case POS_DATA_CODE:
118                        putPDC(ctx, m, mandatory, validFields, rc);
119                        break;
120                    case CAPTURE_DATE:
121                        putCaptureDate(ctx, m, mandatory, validFields, rc);
122                        break;
123                    case AMOUNT:
124                        putAmount(ctx, m, mandatory, validFields, rc);
125                        break;
126                    case ORIGINAL_DATA_ELEMENTS:
127                        putOriginalDataElements(ctx, m, mandatory, validFields, rc);
128                        break;
129                    default:
130                        k = null;
131                }
132            }
133            if (k == null) {
134                if (mandatory && !m.hasField(s))
135                    rc.fail(CMF.MISSING_FIELD, Caller.info(), s);
136                else
137                    validFields.add(s);
138            }
139        }
140    }
141    private void assertNoExtraFields (ISOMsg m, Set validFields, Result rc) {
142        StringBuilder sb = new StringBuilder();
143        for (int i=1; i<=m.getMaxField(); i++) { // we start at 1, MTI is always valid
144            String s = Integer.toString (i);
145            if (m.hasField(i) && !validFields.contains (s)) {
146                if (sb.length() > 0)
147                    sb.append (' ');
148                sb.append (s);
149            }
150        }
151        if (sb.length() > 0)
152            rc.fail(CMF.EXTRA_FIELD, Caller.info(), sb.toString());
153    }
154
155    private void putCard (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
156        boolean hasCard = m.hasAny("2", "14", "35", "45");
157        if (!mandatory && !hasCard)
158            return; // nothing to do, card is optional
159
160        try {
161            Card.Builder cb = Card.builder();
162            if (track1Pattern != null)
163                cb.withTrack1Builder(Track1.builder().pattern(track1Pattern));
164            if (track2Pattern != null)
165                cb.withTrack2Builder(Track2.builder().pattern(track2Pattern));
166            cb.isomsg(m);
167            if (ignoreCardValidation)
168                cb.validator(null);
169            Card card = cb.build();
170            ctx.put (ContextConstants.CARD.toString(), card);
171            if (card.hasTrack1())
172                validFields.add("45");
173            if (card.hasTrack2())
174                validFields.add("35");
175            if (card.getPan() != null && m.hasField(2))
176                validFields.add("2");
177            if (card.getExp() != null && m.hasField(14))
178                validFields.add("14");
179        } catch (InvalidCardException e) {
180            validFields.addAll(Arrays.asList("2", "14", "35", "45"));
181            if (hasCard) {
182                rc.fail(CMF.INVALID_CARD_NUMBER, Caller.info(), e.getMessage());
183            } else if (mandatory) {
184                rc.fail(CMF.MISSING_FIELD, Caller.info(), e.getMessage());
185            }
186        }
187    }
188
189    private void putPCode (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
190        if (m.hasField(3)) {
191            String s = m.getString(3);
192
193            validFields.add("3");
194            if (PCODE_PATTERN.matcher(s).matches()) {
195                ctx.put(ContextConstants.PCODE.toString(), m.getString(3));
196            } else
197                rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid PCODE '%s'", s);
198        } else if (mandatory) {
199            rc.fail(CMF.MISSING_FIELD, Caller.info(), "PCODE");
200        }
201    }
202
203    private void putTid (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
204        if (m.hasField(41)) {
205            String s = m.getString(41);
206            validFields.add("41");
207            if (TID_PATTERN.matcher(s).matches()) {
208                ctx.put(ContextConstants.TID.toString(), m.getString(41));
209            } else
210                rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid TID '%s'", s);
211        } else if (mandatory) {
212            rc.fail(CMF.MISSING_FIELD, Caller.info(), "TID");
213        }
214    }
215
216    private void putMid (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
217        if (m.hasField(42)) {
218            String s = m.getString(42);
219            validFields.add("42");
220            if (MID_PATTERN.matcher(s).matches()) {
221                ctx.put(ContextConstants.MID.toString(), m.getString(42));
222            } else
223                rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid MID '%s'", s);
224        } else if (mandatory) {
225            rc.fail(CMF.MISSING_FIELD, Caller.info(), "MID");
226        }
227    }
228    private void putTimestamp (Context ctx, ISOMsg m, String key, int fieldNumber, Pattern ptrn, boolean mandatory, Set<String> validFields, Result rc) {
229        if (m.hasField(fieldNumber)) {
230            String s = m.getString(fieldNumber);
231            validFields.add(Integer.toString(fieldNumber));
232            if (ptrn.matcher(s).matches())
233                ctx.put (key, ISODate.parseISODate(s));
234            else
235                rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", key, s);
236        } else if (mandatory) {
237            rc.fail(CMF.MISSING_FIELD, Caller.info(), TRANSMISSION_TIMESTAMP.toString());
238        }
239    }
240
241    private void putCaptureDate (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
242        if (m.hasField(17)) {
243            String s = m.getString(17);
244            validFields.add("17");
245            if (CAPTUREDATE_PATTERN.matcher(s).matches())
246                ctx.put (CAPTURE_DATE.toString(), ISODate.parseISODate(s + "120000"));
247            else
248                rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", CAPTURE_DATE, s);
249        } else if (mandatory) {
250            rc.fail(CMF.MISSING_FIELD, Caller.info(), CAPTURE_DATE.toString());
251        }
252    }
253
254    private void putPDC (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
255        if (m.hasField(22)) {
256            byte[] b = m.getBytes(22);
257            validFields.add("22");
258            if (b.length != 16) {
259                rc.fail(
260                  CMF.INVALID_FIELD,
261                  Caller.info(), "Invalid %s '%s'",
262                  ContextConstants.POS_DATA_CODE.toString(),
263                  ISOUtil.hexString(b)
264                );
265            }
266            else {
267                ctx.put(ContextConstants.POS_DATA_CODE.toString(), PosDataCode.valueOf(m.getBytes(22)));
268            }
269        } else if (mandatory) {
270            rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.POS_DATA_CODE.toString());
271        }
272    }
273
274    private void putAmount (Context ctx, ISOMsg m, boolean mandatory,Set<String> validFields, Result rc) {
275        Object o4 = m.getComponent(4);
276        Object o5 = m.getComponent(5);
277        ISOAmount a4 = null;
278        ISOAmount a5 = null;
279        if (o4 instanceof ISOAmount) {
280            a4 = (ISOAmount) o4;
281            validFields.add("4");
282        }
283        if (o5 instanceof ISOAmount) {
284            a5 = (ISOAmount) o5;
285            validFields.add("5");
286        }
287        if (a5 != null) {
288            ctx.put (AMOUNT.toString(), a5);
289            if (a4 != null) {
290                ctx.put (LOCAL_AMOUNT.toString(), a4);
291            }
292        } else if (a4 != null) {
293            ctx.put (AMOUNT.toString(), a4);
294        }
295        if (mandatory && (a4 == null && a5 == null))
296            rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.AMOUNT.toString());
297    }
298
299    private void putOriginalDataElements (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
300        String s = m.getString(56);
301        if (s != null) {
302            validFields.add("56");
303            if (ORIGINAL_DATA_ELEMENTS_PATTERN.matcher(s).matches()) {
304                ctx.put (ORIGINAL_MTI.toString(), s.substring(0,4));
305                ctx.put (ORIGINAL_STAN.toString(), s.substring(4,16));
306                ctx.put (ORIGINAL_TIMESTAMP.toString(), ISODate.parseISODate (s.substring(16,30)));
307            } else {
308                rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", ORIGINAL_DATA_ELEMENTS, s);
309            }
310        } else if (mandatory) {
311            rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.ORIGINAL_DATA_ELEMENTS.toString());
312        }
313    }
314}