Skip to content

Commit e8813a3

Browse files
committed
Added CheckFields participant
1 parent b2a9bce commit e8813a3

File tree

5 files changed

+702
-0
lines changed

5 files changed

+702
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
=== CheckFields participant
2+
3+
The `org.jpos.transaction.participant.CheckFields` is a general
4+
participant that can be used to check for mandatory as well as
5+
optional fields present in the context.
6+
7+
8+
.CheckFields Configuration Properties
9+
[cols="1,2,2", options="header"]
10+
|========================================================================
11+
|Property | Description | Default Value
12+
|request | Name of the ISOMsg to be checked | `REQUEST`
13+
|========================================================================
14+
15+
Here is a sample configuration:
16+
17+
[source,xml]
18+
------------
19+
<participant class="org.jpos.transaction.participant.CheckFields" logger="Q2">
20+
<property name="mandatory" value="PCODE,TRANSMISSION_TIMESTAMP,11,12,AMOUNT,CARD,41" />
21+
<property name="optional" value="15,17,21,22,24,32,37,42,43,46,60,63,62,111,113" />
22+
...
23+
...
24+
</participant>
25+
------------
26+
27+
The `CheckFields` handle standard numeric fields performing minimum validations (i.e. `7,11,12,35`),
28+
it just checks for presence of those fields, but it handle some special names that are relevant to
29+
most jPOS applications, specially those dealing with http://jpos.org/doc/jPOS-CMF.pdf[jPOS-CMF].
30+
31+
In those situations, `CheckFields` performs additional parsing, validation, and places in the
32+
Context handy objects that other participants can use.
33+
34+
For example, if we use the name `CARD`, then `CheckFields` participant tries to get us a `Card`
35+
object taking it from either fields 2 and 14 (manual entry) as well as 35 (track2) or 45 (track1).
36+
In addition, it verifies that track1 and track2 are valid, and matches the PAN and EXP values
37+
present in fields 2 and 14 (if available).
38+
39+
The complete list of special names are:
40+
41+
* `PCODE` - parses the processing code.
42+
* `CARD` - creates a `org.jpos.core.Card` Object.
43+
* `TID` - Terminal ID picked from field 41.
44+
* `MID` - Merchant ID picked from field 42.
45+
* `TRANSMISSION_TIMESTAMP` - creates a Date object picked from field 7 (ISO-8583 v2003 format).
46+
* `TRANSACTION_TIMESTAMP` - creates a Date object picked from field 12 (ISO-8583 v2003 format).
47+
* `POS_DATA_CODE` - create a POSDataCode from field 22.
48+
* `CAPTURE_DATE` - date object picked from field 17
49+
* `AMOUNT` - picks `ISOAmount` from either field 4 or 5. If field 5 is available, then `AMOUNT` holds the content of field 5 (settlement amount)
50+
while field 4 gets stored in another Context variable called `LOCAL_AMOUNT` (ISO-8583 v2003 format).
51+
* `ORIGINAL_DATA_ELEMENTS` parses original MTI, STAN and TIMESTAMP from field 56 (ISO-8583 v2003 format).
52+
53+

doc/src/asciidoc/ch09/transaction_participants.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ jPOS comes with some general purpose transaction participant implementations tha
44
can be used as-is or used as a reference to write your own.
55

66
include::participants/switch.adoc[]
7+
include::participants/check_fields.adoc[]
78
include::participants/send_response.adoc[]
89
include::participants/jsparticipant.adoc[]
910

jpos/src/main/java/org/jpos/transaction/ContextConstants.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public enum ContextConstants {
3636
POS_DATA_CODE,
3737
AMOUNT,
3838
LOCAL_AMOUNT,
39+
ORIGINAL_MTI,
40+
ORIGINAL_STAN,
41+
ORIGINAL_TIMESTAMP,
42+
ORIGINAL_DATA_ELEMENTS,
3943
PAUSED_TRANSACTION(":paused_transaction");
4044

4145
private final String name;
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/*
2+
* jPOS Project [http://jpos.org]
3+
* Copyright (C) 2000-2017 jPOS Software SRL
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.jpos.transaction.participant;
20+
21+
import java.util.Arrays;
22+
import java.util.Set;
23+
import java.util.HashSet;
24+
import java.util.StringTokenizer;
25+
import java.io.Serializable;
26+
import java.util.regex.Pattern;
27+
28+
import org.jpos.core.*;
29+
import org.jpos.iso.*;
30+
import org.jpos.rc.CMF;
31+
import org.jpos.rc.Result;
32+
import org.jpos.transaction.Context;
33+
import org.jpos.transaction.ContextConstants;
34+
import org.jpos.transaction.TransactionParticipant;
35+
import org.jpos.util.Caller;
36+
37+
import static org.jpos.transaction.ContextConstants.*;
38+
39+
public class CheckFields implements TransactionParticipant, Configurable {
40+
private Configuration cfg;
41+
private String request;
42+
private Pattern PCODE_PATTERN = Pattern.compile("^[\\d|\\w]{6}$");
43+
private Pattern TID_PATTERN = Pattern.compile("^\\w{1,16}");
44+
private Pattern MID_PATTERN = Pattern.compile("^\\w{1,15}");
45+
private Pattern TIMESTAMP_PATTERN = Pattern.compile("^\\d{10}");
46+
private Pattern CAPTUREDATE_PATTERN = Pattern.compile("^\\d{4}");
47+
private Pattern ORIGINAL_DATA_ELEMENTS_PATTERN = Pattern.compile("^\\d{30,41}$");
48+
49+
public int prepare (long id, Serializable context) {
50+
Context ctx = (Context) context;
51+
Result rc = ctx.getResult();
52+
try {
53+
ISOMsg m = (ISOMsg) ctx.get (request);
54+
if (m == null) {
55+
ctx.getResult().fail(CMF.INVALID_TRANSACTION, Caller.info(), "'%s' not available in Context", request);
56+
return ABORTED | NO_JOIN | READONLY;
57+
}
58+
Set<String> validFields = new HashSet<>();
59+
assertFields (ctx, m, cfg.get ("mandatory", ""), true, validFields, rc);
60+
assertFields (ctx, m, cfg.get ("optional", ""), false, validFields, rc);
61+
assertNoExtraFields (m, validFields, rc);
62+
} catch (Throwable t) {
63+
rc.fail(CMF.SYSTEM_ERROR, Caller.info(), t.getMessage());
64+
ctx.log(t);
65+
}
66+
return (rc.hasFailures() ? ABORTED : PREPARED) | NO_JOIN | READONLY;
67+
}
68+
69+
public void setConfiguration (Configuration cfg) {
70+
this.cfg = cfg;
71+
request = cfg.get ("request", ContextConstants.REQUEST.toString());
72+
}
73+
74+
private void assertFields(Context ctx, ISOMsg m, String fields, boolean mandatory, Set<String> validFields, Result rc) {
75+
StringTokenizer st = new StringTokenizer (fields, ", ");
76+
while (st.hasMoreTokens()) {
77+
String s = st.nextToken();
78+
ContextConstants k = null;
79+
try {
80+
k = ContextConstants.valueOf(s);
81+
} catch (IllegalArgumentException ignored) { }
82+
if (k != null) {
83+
switch (k) {
84+
case PCODE:
85+
putPCode(ctx, m, mandatory, validFields, rc);
86+
break;
87+
case CARD:
88+
putCard(ctx, m, mandatory, validFields, rc);
89+
break;
90+
case TID:
91+
putTid(ctx, m, mandatory, validFields, rc);
92+
break;
93+
case MID:
94+
putMid(ctx, m, mandatory, validFields, rc);
95+
break;
96+
case TRANSMISSION_TIMESTAMP:
97+
putTimestamp(ctx, m, TRANSMISSION_TIMESTAMP.toString(), 7, mandatory, validFields, rc);
98+
break;
99+
case TRANSACTION_TIMESTAMP:
100+
putTimestamp(ctx, m, TRANSACTION_TIMESTAMP.toString(), 12, mandatory, validFields, rc);
101+
break;
102+
case POS_DATA_CODE:
103+
putPDC(ctx, m, mandatory, validFields, rc);
104+
break;
105+
case CAPTURE_DATE:
106+
putCaptureDate(ctx, m, mandatory, validFields, rc);
107+
break;
108+
case AMOUNT:
109+
putAmount(ctx, m, mandatory, validFields, rc);
110+
break;
111+
case ORIGINAL_DATA_ELEMENTS:
112+
putOriginalDataElements(ctx, m, mandatory, validFields, rc);
113+
break;
114+
default:
115+
k = null;
116+
}
117+
}
118+
if (k == null) {
119+
if (mandatory && !m.hasField(s))
120+
rc.fail(CMF.MISSING_FIELD, Caller.info(), s);
121+
else
122+
validFields.add(s);
123+
}
124+
}
125+
}
126+
private void assertNoExtraFields (ISOMsg m, Set validFields, Result rc) {
127+
StringBuffer sb = new StringBuffer();
128+
for (int i=1; i<=m.getMaxField(); i++) { // we start at 1, MTI is always valid
129+
String s = Integer.toString (i);
130+
if (m.hasField(i) && !validFields.contains (s)) {
131+
if (sb.length() > 0)
132+
sb.append (' ');
133+
sb.append (s);
134+
}
135+
}
136+
if (sb.length() > 0)
137+
rc.fail(CMF.EXTRA_FIELD, Caller.info(), sb.toString());
138+
}
139+
140+
private void putCard (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
141+
try {
142+
Card card = Card.builder().isomsg(m).build();
143+
ctx.put (ContextConstants.CARD.toString(), card);
144+
if (card.hasTrack1())
145+
validFields.add("45");
146+
if (card.hasTrack2())
147+
validFields.add("35");
148+
if (card.getPan() != null && m.hasField(2))
149+
validFields.add("2");
150+
if (card.getExp() != null && m.hasField(14))
151+
validFields.add("14");
152+
} catch (InvalidCardException e) {
153+
validFields.addAll(Arrays.asList("2", "14", "35", "45"));
154+
if (mandatory) {
155+
rc.fail((m.hasAny("2", "14", "35", "45") ? CMF.INVALID_CARD_NUMBER : CMF.MISSING_FIELD),
156+
Caller.info(), e.getMessage());
157+
}
158+
else
159+
rc.warn(Caller.info(), e.getMessage());
160+
}
161+
}
162+
163+
private void putPCode (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
164+
if (m.hasField(3)) {
165+
String s = m.getString(3);
166+
validFields.add("3");
167+
if (PCODE_PATTERN.matcher(s).matches()) {
168+
ctx.put(ContextConstants.PCODE.toString(), m.getString(3));
169+
} else
170+
rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid PCODE '%s'", s);
171+
} else if (mandatory) {
172+
rc.fail(CMF.MISSING_FIELD, Caller.info(), "PCODE");
173+
}
174+
}
175+
176+
private void putTid (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
177+
if (m.hasField(41)) {
178+
String s = m.getString(41);
179+
validFields.add("41");
180+
if (TID_PATTERN.matcher(s).matches()) {
181+
ctx.put(ContextConstants.TID.toString(), m.getString(41));
182+
} else
183+
rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid TID '%s'", s);
184+
} else if (mandatory) {
185+
rc.fail(CMF.MISSING_FIELD, Caller.info(), "TID");
186+
}
187+
}
188+
189+
private void putMid (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
190+
if (m.hasField(42)) {
191+
String s = m.getString(42);
192+
validFields.add("42");
193+
if (MID_PATTERN.matcher(s).matches()) {
194+
ctx.put(ContextConstants.MID.toString(), m.getString(41));
195+
} else
196+
rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid MID '%s'", s);
197+
} else if (mandatory) {
198+
rc.fail(CMF.MISSING_FIELD, Caller.info(), "MID");
199+
}
200+
}
201+
private void putTimestamp (Context ctx, ISOMsg m, String key, int fieldNumber, boolean mandatory, Set<String> validFields, Result rc) {
202+
if (m.hasField(fieldNumber)) {
203+
String s = m.getString(fieldNumber);
204+
validFields.add(Integer.toString(fieldNumber));
205+
if (TIMESTAMP_PATTERN.matcher(s).matches())
206+
ctx.put (key, ISODate.parseISODate(s));
207+
else
208+
rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", key, s);
209+
} else if (mandatory) {
210+
rc.fail(CMF.MISSING_FIELD, Caller.info(), TRANSMISSION_TIMESTAMP.toString());
211+
}
212+
}
213+
214+
private void putCaptureDate (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
215+
if (m.hasField(17)) {
216+
String s = m.getString(17);
217+
validFields.add("17");
218+
if (CAPTUREDATE_PATTERN.matcher(s).matches())
219+
ctx.put (CAPTURE_DATE.toString(), ISODate.parseISODate(s + "120000"));
220+
else
221+
rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", CAPTURE_DATE, s);
222+
} else if (mandatory) {
223+
rc.fail(CMF.MISSING_FIELD, Caller.info(), CAPTURE_DATE.toString());
224+
}
225+
}
226+
227+
private void putPDC (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
228+
if (m.hasField(22)) {
229+
byte[] b = m.getBytes(22);
230+
validFields.add("22");
231+
if (b.length != 16) {
232+
rc.fail(
233+
CMF.INVALID_FIELD,
234+
Caller.info(), "Invalid %s '%s'",
235+
ContextConstants.POS_DATA_CODE.toString(),
236+
ISOUtil.hexString(b)
237+
);
238+
}
239+
else {
240+
ctx.put(ContextConstants.POS_DATA_CODE.toString(), PosDataCode.valueOf(m.getBytes(22)));
241+
}
242+
} else if (mandatory) {
243+
rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.POS_DATA_CODE.toString());
244+
}
245+
}
246+
247+
private void putAmount (Context ctx, ISOMsg m, boolean mandatory,Set<String> validFields, Result rc) {
248+
Object o4 = m.getComponent(4);
249+
Object o5 = m.getComponent(5);
250+
ISOAmount a4 = null;
251+
ISOAmount a5 = null;
252+
if (o4 instanceof ISOAmount) {
253+
a4 = (ISOAmount) o4;
254+
validFields.add("4");
255+
}
256+
if (o5 instanceof ISOAmount) {
257+
a5 = (ISOAmount) o5;
258+
validFields.add("5");
259+
}
260+
if (a5 != null) {
261+
ctx.put (AMOUNT.toString(), a5);
262+
if (a4 != null) {
263+
ctx.put (LOCAL_AMOUNT.toString(), a4);
264+
}
265+
} else if (a4 != null) {
266+
ctx.put (AMOUNT.toString(), a4);
267+
}
268+
if (mandatory && (a4 == null && a5 == null))
269+
rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.AMOUNT.toString());
270+
}
271+
272+
private void putOriginalDataElements (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) {
273+
String s = m.getString(56);
274+
if (s != null) {
275+
validFields.add("56");
276+
if (ORIGINAL_DATA_ELEMENTS_PATTERN.matcher(s).matches()) {
277+
ctx.put (ORIGINAL_MTI.toString(), s.substring(0,4));
278+
ctx.put (ORIGINAL_STAN.toString(), s.substring(4,16));
279+
ctx.put (ORIGINAL_TIMESTAMP.toString(), ISODate.parseISODate (s.substring(16,30)));
280+
} else {
281+
rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", ORIGINAL_DATA_ELEMENTS, s);
282+
}
283+
} else if (mandatory) {
284+
rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.ORIGINAL_DATA_ELEMENTS.toString());
285+
}
286+
}
287+
}

0 commit comments

Comments
 (0)