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