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 org.jdom2.Element; 022import org.jpos.core.*; 023import org.jpos.iso.ISOMsg; 024import org.jpos.q2.Q2; 025import org.jpos.q2.QFactory; 026import org.jpos.rc.CMF; 027import org.jpos.transaction.Context; 028import org.jpos.transaction.ContextConstants; 029import org.jpos.transaction.TransactionParticipant; 030import org.jpos.util.Caller; 031import org.jpos.util.Log; 032import org.jpos.util.LogEvent; 033import org.jpos.util.Logger; 034 035import java.io.Serializable; 036import java.math.BigInteger; 037import java.util.*; 038import java.util.regex.Matcher; 039import java.util.regex.Pattern; 040 041@SuppressWarnings("unused") 042public class SelectDestination implements TransactionParticipant, Configurable, XmlConfigurable { 043 private String requestName; 044 private String destinationName; 045 private String defaultDestination; 046 private boolean failOnNoRoute; 047 private CardValidator validator; 048 private Set<BinRange> binranges = new TreeSet<>(); 049 private List<PanRegExp> regexps = new ArrayList<>(); 050 051 @Override 052 public int prepare(long id, Serializable context) { 053 Context ctx = (Context) context; 054 ISOMsg m = (ISOMsg) ctx.get(requestName); 055 boolean destinationSet = false; 056 if (m != null && (m.hasField(2) || m.hasField(35))) { 057 try { 058 Card card = Card.builder().validator(validator).isomsg(m).build(); 059 String destination = getDestination(card); 060 if (destination != null) { 061 ctx.put(destinationName, destination); 062 destinationSet = true; 063 } 064 } catch (InvalidCardException ex) { 065 return ctx.getResult().fail( 066 CMF.INVALID_CARD_OR_CARDHOLDER_NUMBER, Caller.info(), ex.getMessage() 067 ).FAIL(); 068 } 069 } 070 if (!destinationSet && ctx.get(destinationName) == null) 071 ctx.put(destinationName, defaultDestination); 072 if (failOnNoRoute && ctx.get(destinationName) == null) 073 return ctx.getResult().fail(CMF.ROUTING_ERROR, Caller.info(), "No routing info").FAIL(); 074 075 return PREPARED | NO_JOIN | READONLY; 076 } 077 078 public void setConfiguration (Configuration cfg) { 079 this.requestName = cfg.get("request", ContextConstants.REQUEST.toString()); 080 this.destinationName = cfg.get ("destination", ContextConstants.DESTINATION.toString()); 081 this.defaultDestination = cfg.get("default-destination", null); 082 this.validator = cfg.getBoolean("ignore-card-validations") ? 083 new NoCardValidator() : cfg.getBoolean("ignore-luhn") ? 084 new IgnoreLuhnCardValidator() : Card.Builder.DEFAULT_CARD_VALIDATOR; 085 this.failOnNoRoute = cfg.getBoolean("fail"); 086 } 087 088 /** 089 * @param xml Configuration element 090 * @throws ConfigurationException 091 * 092 * 093 * SelectDestination expects an XML configuration in the following format: 094 * 095 * <endpoint destination="XXX"> 096 * 4000000..499999 097 * 4550000..455999 098 * 5 099 * </endpoint> 100 * 101 */ 102 public void setConfiguration(Element xml) throws ConfigurationException { 103 for (Element ep : xml.getChildren("endpoint")) { 104 String destination = QFactory.getAttributeValue(ep, "destination"); 105 StringTokenizer st = new StringTokenizer(Environment.get(ep.getTextTrim())); 106 while (st.hasMoreElements()) { 107 BinRange br = new BinRange(destination, st.nextToken()); 108 binranges.add(br); 109 } 110 } 111 for (Element re : xml.getChildren("regexp")) { 112 String destination = QFactory.getAttributeValue(re, "destination"); 113 regexps.add( 114 new PanRegExp(QFactory.getAttributeValue(re, "destination"), Environment.get(re.getTextTrim())) 115 ); 116 } 117 LogEvent evt = Log.getLog(Q2.LOGGER_NAME, this.getClass().getName()).createLogEvent("config"); 118 for (PanRegExp r : regexps) 119 evt.addMessage("00:" + r); 120 for (BinRange r : binranges) 121 evt.addMessage(r); 122 Logger.log(evt); 123 } 124 125 private String getDestination (Card card) { 126 String destination = getDestinationByRegExp(card.getPan()); 127 if (destination == null) 128 destination = getDestinationByPanNumber(card.getPanAsNumber()); 129 return destination; 130 } 131 132 private String getDestinationByPanNumber (BigInteger pan) { 133 final BigInteger p = BinRange.floor(pan); 134 return binranges 135 .stream() 136 .filter(r -> r.isInRange(p)) 137 .findFirst() 138 .map(BinRange::destination).orElse(null); 139 } 140 141 private String getDestinationByRegExp (String pan) { 142 return regexps 143 .stream() 144 .filter(r -> r.matches(pan)) 145 .findFirst() 146 .map(PanRegExp::destination).orElse(null); 147 } 148 149 public static class BinRange implements Comparable { 150 private String destination; 151 private BigInteger low; 152 private BigInteger high; 153 private short weight; 154 private static Pattern rangePattern = Pattern.compile("^([\\d]{1,19})*(?:\\.\\.)?([\\d]{0,19})?$"); 155 156 /** 157 * Sample bin: 158 * <ul> 159 * <li>4</li> 160 * <li>411111</li> 161 * <li>4111111111111111</li> 162 * <li>411111..422222</li> 163 * </ul> 164 * @param destination range destination 165 * @param rangeExpr either a 'bin', 'extended bin' or 'bin range' 166 */ 167 BinRange(String destination, String rangeExpr) { 168 this.destination = destination; 169 Matcher matcher = rangePattern.matcher(rangeExpr); 170 if (!matcher.matches()) 171 throw new IllegalArgumentException("Invalid range " + rangeExpr); 172 173 String l = matcher.group(1); 174 String h = matcher.group(2); 175 h = h.isEmpty() ? l : h; 176 weight = (short) (Math.max(l.length(), h.length())); 177 low = floor(new BigInteger(l)); 178 high = ceiling(new BigInteger(h)); 179 if (low.compareTo(high) > 0) 180 throw new IllegalArgumentException("Invalid range " + low + "/" + high); 181 } 182 183 @Override 184 public boolean equals(Object o) { 185 if (this == o) return true; 186 if (o == null || getClass() != o.getClass()) return false; 187 BinRange binRange = (BinRange) o; 188 return weight == binRange.weight && 189 Objects.equals(low, binRange.low) && 190 Objects.equals(high, binRange.high); 191 } 192 193 @Override 194 public int hashCode() { 195 return Objects.hash(low, high, weight); 196 } 197 198 @Override 199 public String toString() { 200 return String.format("%02d:%s..%s [%s]", 19-weight, low, high, destination); // Warning, compareTo expects sorteable format 201 } 202 @Override 203 public int compareTo(Object o) { 204 return toString().compareTo(o.toString()); 205 } 206 public boolean isInRange (BigInteger i) { 207 return i.compareTo(low) >=0 && i.compareTo(high) <=0; 208 } 209 210 public String destination() { 211 return destination; 212 } 213 214 static BigInteger floor(BigInteger i) { 215 if (!BigInteger.ZERO.equals(i)) { 216 int digits = (int)Math.log10(i.doubleValue())+1; 217 i = i.multiply(BigInteger.TEN.pow(19-digits)); 218 } 219 return i; 220 } 221 private BigInteger ceiling (BigInteger i) { 222 int digits = BigInteger.ZERO.equals(i) ? 1 : (int)Math.log10(i.doubleValue())+1; 223 return floor(i).add(BigInteger.TEN.pow(19-digits)).subtract(BigInteger.ONE); 224 } 225 } 226 227 private static class PanRegExp { 228 private String destination; 229 private Pattern pattern; 230 231 PanRegExp(String destination, String regexp) { 232 this.destination = destination; 233 this.pattern = Pattern.compile(regexp); 234 } 235 236 public String destination() { 237 return destination; 238 } 239 240 public boolean matches(String pan) { 241 return pattern.matcher(pan).matches(); 242 } 243 244 @Override 245 public boolean equals(Object o) { 246 if (this == o) return true; 247 if (o == null || getClass() != o.getClass()) return false; 248 PanRegExp panRegExp = (PanRegExp) o; 249 return Objects.equals(destination, panRegExp.destination) && 250 Objects.equals(pattern, panRegExp.pattern); 251 } 252 253 @Override 254 public int hashCode() { 255 return Objects.hash(destination, pattern); 256 } 257 258 @Override 259 public String toString() { 260 return String.format("%s [%s]", pattern.toString(), destination); 261 } 262 } 263}