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.iso.packager;
020
021import org.jpos.iso.*;
022import org.jpos.iso.header.BaseHeader;
023import org.jpos.util.LogEvent;
024import org.jpos.util.LogSource;
025import org.jpos.util.Logger;
026import org.xml.sax.Attributes;
027import org.xml.sax.InputSource;
028import org.xml.sax.SAXException;
029import org.xml.sax.XMLReader;
030import org.xml.sax.helpers.DefaultHandler;
031import org.xml.sax.helpers.XMLReaderFactory;
032
033import java.io.*;
034import java.math.BigDecimal;
035import java.nio.charset.StandardCharsets;
036import java.util.Stack;
037import java.util.concurrent.locks.Lock;
038import java.util.concurrent.locks.ReentrantLock;
039
040/**
041 * packs/unpacks ISOMsgs into XML representation
042 *
043 * @author apr@cs.com.uy
044 * @version $Id$
045 * @see ISOPackager
046 */
047@SuppressWarnings("unchecked")
048public class XMLPackager extends DefaultHandler
049                         implements ISOPackager, LogSource
050{
051    protected Logger logger = null;
052    protected String realm = null;
053    private XMLReader reader;
054    private Stack stk;
055    private Lock parserLock = new ReentrantLock();
056
057    public static final String ISOMSG_TAG    = "isomsg";
058    public static final String ISOFIELD_TAG  = "field";
059    public static final String ID_ATTR       = "id";
060    public static final String VALUE_ATTR    = "value";
061    public static final String TYPE_ATTR     = "type";
062    public static final String TYPE_BINARY   = "binary";
063    public static final String TYPE_BITMAP   = "bitmap";
064    public static final String TYPE_AMOUNT   = "amount";
065    public static final String CURRENCY_ATTR = "currency";
066    public static final String HEADER_TAG    = "header";
067    public static final String ENCODING_ATTR = "encoding";
068    public static final String ASCII_ENCODING= "ascii";
069
070    // fields that will be forced to be interpreted as binary data
071    private int[] binaryFields= null;
072
073    public XMLPackager() throws ISOException {
074        super();
075        stk = new Stack();
076        try {
077            reader = createXMLReader();
078
079            // some parser restrictions have been set for security and maybe PCI compliance
080            setXMLParserFeature("http://xml.org/sax/features/validation", false);
081            setXMLParserFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
082            setXMLParserFeature("http://xml.org/sax/features/external-general-entities", false);
083            setXMLParserFeature("http://xml.org/sax/features/external-parameter-entities", false);
084        } catch (Exception e) {
085            throw new ISOException (e.toString());
086        }
087    }
088
089    public void forceBinary(int ... bfields) {
090        binaryFields= bfields;
091    }
092
093    public byte[] pack (ISOComponent c) throws ISOException {
094        LogEvent evt = new LogEvent (this, "pack");
095
096        try {
097            if (!(c instanceof ISOMsg m))
098                throw new ISOException ("cannot pack "+c.getClass());
099
100            // heuristics for initial size 40: a typical <field> with 2 chars of id + 12 of value + 2 indent
101            ByteArrayOutputStream out = new ByteArrayOutputStream(40 * (m.getChildren().size()+2));
102            PrintStream p = new PrintStream(out, false, StandardCharsets.UTF_8);
103
104            m.setDirection(0);  // avoid "direction=xxxxxx" in XML msg
105            m.dump (p, "");
106            byte[] b = out.toByteArray();
107
108            if (logger != null)
109                evt.addMessage (m);
110            return b;
111        } catch (ISOException e) {
112            evt.addMessage (e);
113            throw e;
114        } finally {
115            Logger.log(evt);
116        }
117    }
118
119    public int unpack (ISOComponent c, byte[] b) throws ISOException {
120        unpack(c, new InputSource(new ByteArrayInputStream(b)));
121        return b.length;
122    }
123
124    public void unpack (ISOComponent c, InputStream in) throws ISOException, IOException {
125        unpack(c, new InputSource(in));
126    }
127
128    private void unpack (ISOComponent c, InputSource in) throws ISOException {
129        LogEvent evt = new LogEvent (this, "unpack");
130
131        parserLock.lock();
132        try {
133            if (!(c instanceof ISOMsg m))
134                throw new ISOException("Can't call packager on non Composite");
135
136            while (!stk.empty())    // purge from possible previous error
137                stk.pop();
138
139            reader.parse (in);
140            if (stk.empty())
141                throw new ISOException ("error parsing");
142
143            ISOMsg m1 = (ISOMsg) stk.pop();
144            m.merge (m1);
145            m.setHeader (m1.getHeader());
146
147            fixupBinary(m, binaryFields);
148
149            if (logger != null)
150                evt.addMessage (m);
151        } catch (ISOException e) {
152            evt.addMessage (e);
153            throw e;
154        } catch (IOException e) {
155            evt.addMessage (e);
156            throw new ISOException (e.toString());
157        } catch (SAXException e) {
158            evt.addMessage (e);
159            throw new ISOException (e.toString());
160        } finally {
161            Logger.log (evt);
162            parserLock.unlock();
163        }
164    }
165
166    public void startElement
167        (String ns, String name, String qName, Attributes atts)
168        throws SAXException
169    {
170        int fieldNumber = -1;
171        try {
172            String id       = atts.getValue(ID_ATTR);
173            if (id != null) {
174                try {
175                    fieldNumber = Integer.parseInt (id);
176                } catch (NumberFormatException ex) {
177                    throw new SAXException ("Invalid id " + id);
178                }
179            }
180            if (name.equals (ISOMSG_TAG)) {
181                if (fieldNumber >= 0) {
182                    if (stk.empty())
183                        throw new SAXException ("inner without outer");
184
185                    ISOMsg inner = new ISOMsg(fieldNumber);
186                    ((ISOMsg)stk.peek()).set (inner);
187                    stk.push (inner);
188                } else {
189                    stk.push (new ISOMsg(0));
190                }
191            } else if (name.equals (ISOFIELD_TAG)) {
192                ISOMsg m     = (ISOMsg) stk.peek();
193                String value = atts.getValue(VALUE_ATTR);
194                String type  = atts.getValue(TYPE_ATTR);
195                if (id == null)
196                    throw new SAXException ("invalid field");
197                value = value == null ? "" : value;
198
199                ISOComponent ic;
200                if (TYPE_BINARY.equals (type)) {
201                    ic = new ISOBinaryField (
202                        fieldNumber,
203                            ISOUtil.hex2byte (
204                                value.getBytes(), 0, value.length()/2
205                            )
206                        );
207                }
208                else if (TYPE_AMOUNT.equals (type)) {
209                    ic =  new ISOAmount(
210                        fieldNumber,
211                        Integer.parseInt (atts.getValue(CURRENCY_ATTR)),
212                        new BigDecimal (value)
213                    );
214                }
215                else {
216                    ic = new ISOField (fieldNumber, ISOUtil.stripUnicode(value));
217                }
218                m.set (ic);
219                stk.push (ic);
220            } else if (HEADER_TAG.equals (name)) {
221                BaseHeader bh = new BaseHeader();
222                bh.setAsciiEncoding (ASCII_ENCODING.equalsIgnoreCase(atts.getValue(ENCODING_ATTR)));
223                stk.push (bh);
224            }
225        } catch (ISOException e) {
226            throw new SAXException
227                ("ISOException unpacking "+fieldNumber);
228        }
229    }
230
231    public void characters (char ch[], int start, int length) {
232        Object obj = stk.peek();
233        if (obj instanceof ISOField) {
234            ISOField f = (ISOField) obj;
235            String value = f.getValue() + new String(ch, start, length);
236            try {
237                f.setValue(value);
238            } catch (ISOException e) {
239                try {
240                    f.setValue (e.getMessage());
241                } catch (ISOException ignored) {
242                    // giving up
243                }
244            }
245        }
246        else if (obj instanceof BaseHeader) {
247            BaseHeader bh = (BaseHeader) obj;
248            String s = new String(ch,start,length);
249            if (bh.isAsciiEncoding()) {
250                bh.unpack (s.getBytes());
251            } else {
252                bh.unpack (ISOUtil.hex2byte (s));
253            }
254        }
255    }
256
257    public void endElement (String ns, String name, String qname)
258        throws SAXException
259    {
260        if (name.equals (ISOMSG_TAG)) {
261            ISOMsg m = (ISOMsg) stk.pop();
262            if (stk.empty())
263                stk.push (m); // push outer message
264        } else if (ISOFIELD_TAG.equals (name)) {
265            stk.pop();
266        } else if (HEADER_TAG.equals (name)) {
267            BaseHeader h = (BaseHeader) stk.pop();
268            ISOMsg m = (ISOMsg) stk.peek ();
269            m.setHeader (h);
270        }
271    }
272
273    // we may want to force fome fields to be interpreted as binary data
274    protected void fixupBinary(ISOMsg m, int[] bfields) throws ISOException {
275        if (bfields != null) {
276            for (int f : bfields) {
277                if (m.hasField(f)) {
278                    ISOComponent c = m.getComponent(f);
279                    if (c instanceof ISOField)
280                        m.set(f, ((ISOField) c).getBytes());
281                }
282            }
283        }
284    }
285
286    public String getFieldDescription(ISOComponent m, int fldNumber) {
287        return "Data element " + fldNumber;
288    }
289    public void setLogger (Logger logger, String realm) {
290        this.logger = logger;
291        this.realm  = realm;
292    }
293    public String getRealm () {
294        return realm;
295    }
296    public Logger getLogger() {
297        return logger;
298    }
299    public ISOMsg createISOMsg () {
300        return new ISOMsg();
301    }
302    public String getDescription () {
303        return getClass().getName();
304    }
305
306    protected XMLReader createXMLReader () throws SAXException {
307        XMLReader reader;
308        try {
309            reader = XMLReaderFactory.createXMLReader();
310        } catch (SAXException e) {
311            reader = XMLReaderFactory.createXMLReader (
312                System.getProperty(
313                    "org.xml.sax.driver",
314                    "org.apache.crimson.parser.XMLReaderImpl"
315                )
316            );
317        }
318
319        reader.setContentHandler(this);
320        reader.setErrorHandler(this);
321        return reader;
322    }
323
324    public void setXMLParserFeature(String fname, boolean val) throws SAXException  {
325        reader.setFeature(fname, val);
326    }
327}
328