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}