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;
020
021import org.jpos.util.LogEvent;
022import org.jpos.util.LogSource;
023import org.jpos.util.Logger;
024
025import java.io.EOFException;
026import java.io.IOException;
027import java.io.InputStream;
028import java.util.ArrayList;
029import java.util.BitSet;
030import java.util.Map;
031
032/**
033 * provides base functionality for the actual packagers
034 *
035 * @author apr
036 */
037@SuppressWarnings ("unused")
038public abstract class ISOBasePackager implements ISOPackager, LogSource {
039    /** Default constructor; no instance state to initialise. */
040    protected ISOBasePackager() {}
041    /** Per-field packagers indexed by field number. */
042    protected ISOFieldPackager[] fld;
043    /** Field number that carries the tertiary bitmap as a Data Element, or {@code -999} if not used. */
044    protected int thirdBitmapField= -999;       // for implementations where the tertiary bitmap is inside a Data Element
045
046    /** Logger used to record pack/unpack diagnostics; {@code null} disables logging. */
047    protected Logger logger = null;
048    /** When {@code true}, field descriptions are emitted as XML comments in unpack logs. */
049    protected boolean logFieldName= true;
050    /** Logger realm associated with this packager. */
051    protected String realm = null;
052    /** Number of leading bytes treated as the ISO header during unpack. */
053    protected int headerLength = 0;
054
055    /**
056     * Replaces the per-field packager array.
057     *
058     * @param fld new array of field packagers indexed by field number
059     */
060    public void setFieldPackager (ISOFieldPackager[] fld) {
061        this.fld = fld;
062    }
063
064    /**
065     * Configures the Data Element field that holds the tertiary bitmap.
066     *
067     * @param f field number in the range [0, 128]
068     * @throws ISOException if {@code f} is outside the valid range
069     */
070    public void setThirdBitmapField(int f) throws ISOException
071    {
072        if (f < 0 || f > 128)
073            throw new ISOException("thirdBitmapField should be >= 0 and <= 128");
074        thirdBitmapField= f;
075    }
076    /**
077     * Returns the field number carrying the tertiary bitmap as a Data Element.
078     *
079     * @return the configured third-bitmap field number, or {@code -999} if not set
080     */
081    public int getThirdBitmapField() { return thirdBitmapField; }
082
083    /**
084     * Indicates whether a primary bitmap is emitted on pack.
085     *
086     * @return {@code true} if the field-1 packager is an {@link ISOBitMapPackager}
087     */
088    protected boolean emitBitMap () {
089        return fld[1] instanceof ISOBitMapPackager;
090    }
091
092    /**
093     * usually 2 for normal fields, 1 for bitmap-less or ANSI X9.2
094     * @return first valid field
095     */
096    protected int getFirstField() {
097        if (!(fld[0] instanceof ISOMsgFieldPackager) && fld.length > 1)
098            return fld[1] instanceof ISOBitMapPackager ? 2 : 1;
099        return 0;
100    }
101
102    /**
103     * pack method that works in conjunction with {@link #unpack(ISOComponent, byte[])}.
104     * <p>
105     * Handles a tertiary bitmap possibly appearing in Data Element {@code thirdBitmapField}.<br>
106     *
107     * @param   m   the Component to pack
108     * @return      Message image
109     * @exception ISOException if the component cannot be packed
110     */
111    @Override
112    public byte[] pack (ISOComponent m) throws ISOException
113    {
114        LogEvent evt = null;
115        if (logger != null)
116            evt = new LogEvent (this, "pack");
117
118        try {
119            if (m.getComposite() != m)
120                throw new ISOException ("Can't call packager on non Composite");
121
122            ArrayList<byte[]> v = new ArrayList<byte[]>(128);
123            byte[] b;
124            byte[] hdr= null;
125            int len = 0;
126
127            Map fields = m.getChildren();
128            ISOComponent c = (ISOComponent) fields.get (0);
129            int first = getFirstField();
130
131
132            // pre-read header, if it exists, and advance total len
133            if (m instanceof ISOMsg && headerLength>0)
134            {
135                hdr= ((ISOMsg) m).getHeader();
136                if (hdr != null)
137                        len += hdr.length;
138            }
139
140            if (first > 0 && c != null) {
141                b = fld[0].pack(c);
142                len += b.length;
143                v.add (b);
144            }
145
146            BitSet bmap12= null;                            // will store primary and secondary part of bitmap
147            BitSet bmap3= null;                             // will store tertiary part of bitmap
148            if (emitBitMap())
149            {   // The ISOComponent stores a single bitmap in field -1, which could be up to
150                // 192 bits long. If we have a thirdBitmapField, we may need to split the full
151                // bitmap into 1 & 2 at the beginning (16 bytes), and 3rd inside the Data Element
152                c = (ISOComponent) fields.get (-1);
153                bmap12= (BitSet)c.getValue();               // the full bitmap (up to 192 bits long)
154
155                if (thirdBitmapField >= 0 &&                // we may need to split it!
156                    fld[thirdBitmapField] instanceof ISOBitMapPackager)
157                {
158                    if (bmap12.length() - 1 > 128)          // some bits are set in the high part (3rd bitmap)
159                    {
160                        bmap3= bmap12.get(128, 193);        // new bitmap, with the high 3rd bitmap (use 128 as dummy bit0)
161                        bmap3.clear(0);                     // don't really need to clear dummy bit0 I guess...
162                        bmap12.set(thirdBitmapField);       // indicate presence of field that will hold the 3rd bitmap
163                        bmap12.clear(129, 193);             // clear high part, so that the field's pack() method will not use it
164
165                        // Now create add-hoc ISOBitMap in position thirdBitmapField to hold 3rd bitmap
166                        ISOBitMap bmField= new ISOBitMap(thirdBitmapField);
167                        bmField.setValue(bmap3);
168                        m.set(bmField);
169                        fields.put(thirdBitmapField, bmField);    // fields is a clone of m's inner map, so we store it here as well
170
171                        // bit65 should only be set if there's a data-containing DE-65 (which should't happen!)
172                        bmap12.set(65, fields.get(65) == null ? false : true);
173                    }
174                    else
175                    {   // else: No bits/fields above 128 in this message.
176                        // In case there's an old (residual/garbage) field `thirdBitmapField` in the message
177                        // we need to clear the bit and the data
178                        m.unset(thirdBitmapField);                // remove from ISOMsg
179                        bmap12.clear(thirdBitmapField);           // remove from inner bitmap
180                        fields.remove(thirdBitmapField);          // remove from fields clone
181                    }
182                }
183                // now will emit the 1st and 2nd bitmaps, and the loop below will take care of 3rd
184                // when emitting field `thirdBitmapField`
185                b = getBitMapfieldPackager().pack(c);
186                len += b.length;
187                v.add (b);
188            }
189
190            // if Field 1 is a BitMap then we are packing an
191            // ISO-8583 message so next field is fld#2.
192            // else we are packing an ANSI X9.2 message, first field is 1
193            int tmpMaxField=Math.min (m.getMaxField(), (bmap3 != null || fld.length > 129) ? 192 : 128);
194
195            for (int i=first; i<=tmpMaxField; i++) {
196                if ((c=(ISOComponent) fields.get (i)) != null)
197                {
198                    try {
199                        ISOFieldPackager fp = fld[i];
200                        if (fp == null)
201                            throw new ISOException ("null field "+i+" packager");
202                        b = fp.pack(c);
203                        len += b.length;
204                        v.add (b);
205                    } catch (ISOException e) {
206                        if (evt != null) {
207                            evt.addMessage ("error packing field "+i);
208                            evt.addMessage (c);
209                            evt.addMessage (e);
210                        }
211                        throw new ISOException("error packing field "+i, e);
212                    }
213                }
214            }
215
216            int k = 0;
217            byte[] d = new byte[len];
218
219            // if ISOMsg insert header (we pre-read it at the beginning)
220            if (hdr != null) {
221                System.arraycopy(hdr, 0, d, k, hdr.length);
222                k += hdr.length;
223            }
224
225            for (byte[] bb : v) {
226                System.arraycopy(bb, 0, d, k, bb.length);
227                k += bb.length;
228            }
229            if (evt != null)  // save a few CPU cycle if no logger available
230                evt.addMessage (ISOUtil.hexString (d));
231
232            return d;
233        } catch (ISOException e) {
234            if (evt != null)
235                evt.addMessage (e);
236            throw e;
237        } finally {
238            if (evt != null)
239                Logger.log(evt);
240        }
241    }
242
243    /**
244     * @param   m   the Container of this message
245     * @param   b   ISO message image
246     * @return      consumed bytes
247     * @exception ISOException if the message image cannot be unpacked
248     */
249    @Override
250    public int unpack (ISOComponent m, byte[] b) throws ISOException {
251        LogEvent evt = logger != null ? new LogEvent (this, "unpack") : null;
252        int consumed = 0;
253
254        try {
255            if (m.getComposite() != m)
256                throw new ISOException ("Can't call packager on non Composite");
257            if (evt != null)  // save a few CPU cycle if no logger available
258                evt.addMessage (ISOUtil.hexString (b));
259
260
261            // if ISOMsg and headerLength defined
262            if (m instanceof ISOMsg /*&& ((ISOMsg) m).getHeader()==null*/ && headerLength>0)
263            {
264                byte[] h = new byte[headerLength];
265                System.arraycopy(b, 0, h, 0, headerLength);
266                ((ISOMsg) m).setHeader(h);
267                consumed += headerLength;
268            }
269
270            if (!(fld[0] == null) && !(fld[0] instanceof ISOBitMapPackager))
271            {
272                ISOComponent mti = fld[0].createComponent(0);
273                consumed  += fld[0].unpack(mti, b, consumed);
274                m.set (mti);
275            }
276
277            BitSet bmap = null;
278            int bmapBytes= 0;                                   // bitmap length in bytes (usually 8, 16, 24)
279            int maxField= fld.length - 1;                       // array length counts position 0!
280
281            if (emitBitMap()) {
282                ISOBitMap bitmap = new ISOBitMap (-1);
283                consumed += getBitMapfieldPackager().unpack(bitmap,b,consumed);
284                bmap = (BitSet) bitmap.getValue();
285                bmapBytes= (bmap.length()-1 + 63) >> 6 << 3;
286                if (evt != null)
287                    evt.addMessage ("<bitmap>"+bmap.toString()+"</bitmap>");
288                m.set (bitmap);
289
290                maxField = Math.min(maxField, bmap.length()-1); // bmap.length behaves similarly to fld.length
291            }
292
293            for (int i= getFirstField(); i <= maxField; i++) {
294                try {
295                    if (bmap == null && fld[i] == null)
296                        continue;
297
298                    // maxField is computed above as min(fld.length-1, bmap.length()-1), therefore
299                    // "maxField > 128" means fld[] has packagers defined above 128, *and*
300                    // the bitmap's length is greater than 128 (i.e., a contiguous tertiary bitmap exists).
301                    // In this case, bit 65 simply indicates a 3rd bitmap contiguous to the 2nd one.
302                    // Therefore, there MUST NOT be a DE-65 with data payload to read.
303                    if (maxField > 128 && i==65)
304                        continue;   // ignore extended bitmap
305
306                    if (bmap == null || bmap.get(i)) {
307                        if (fld[i] == null)
308                            throw new ISOException ("field packager '" + i + "' is null");
309
310                        ISOComponent c = fld[i].createComponent(i);
311                        consumed += fld[i].unpack (c, b, consumed);
312                        if (evt != null)
313                            fieldUnpackLogger(evt, i, c, fld[i], logFieldName);
314                        m.set(c);
315
316                        if (i == thirdBitmapField && fld.length > 129 &&          // fld[128] is at pos 129
317                            bmapBytes == 16 &&
318                            fld[thirdBitmapField] instanceof ISOBitMapPackager)
319                        {   // We have a weird case of tertiary bitmap implemented inside a Data Element
320                            // instead of being contiguous to the primary and secondary bitmaps.
321                            // If enter this "if" it's because we have a proper 16-byte bitmap (1st & 2nd),
322                            // but are expecting more than 128 Data Elements according to fld[].
323                            // Normally, these kind of ISO8583 implementations have the tertiary bitmap in DE-65,
324                            // but sometimes they specify some other DE (given by thirdBitmapField).
325                            // We also double check that the DE has been specified as an ISOBitMapPackager in fld[].
326                            // By now, the tertiary bitmap has already been unpacked into field `thirdBitmapField`.
327                            BitSet bs3rd= (BitSet)((ISOComponent)m.getChildren().get(thirdBitmapField)).getValue();
328                            maxField= 128 + (bs3rd.length() - 1);                 // update loop end condition
329                            for (int bit= 1; bit <= 64; bit++)
330                                bmap.set(bit+128, bs3rd.get(bit));                // extend bmap with new bits above 128
331                        }
332                    }
333                } catch (ISOException e) {
334                    if (evt != null) {
335                        evt.addMessage("error unpacking field " + i + " consumed=" + consumed);
336                        evt.addMessage(e);
337                    }
338                    // jPOS-3
339                    if (e.getNested() == null) {
340                        e = new ISOException(
341                            String.format("%s unpacking field=%d, consumed=%d",
342                            e.getMessage(), i, consumed)
343                        );
344                    } else {
345                        e = new ISOException(
346                            String.format("%s (%s) unpacking field=%d, consumed=%d",
347                            e.getMessage(), e.getNested().toString(), i, consumed)
348                        );
349                    }
350                    throw e;
351                }
352            } // for each field
353
354            if (evt != null && b.length != consumed) {
355                evt.addMessage ("WARNING: unpack len=" +b.length +" consumed=" +consumed);
356            }
357
358            return consumed;
359        } catch (ISOException e) {
360            if (evt != null)
361                evt.addMessage (e);
362            throw e;
363        } catch (Exception e) {
364            if (evt != null)
365                evt.addMessage (e);
366            throw new ISOException (e.getMessage() + " consumed=" + consumed);
367        } finally {
368            if (evt != null)
369                Logger.log (evt);
370        }
371    }
372
373    public void unpack (ISOComponent m, InputStream in)
374        throws IOException, ISOException
375    {
376        LogEvent evt = logger != null ? new LogEvent (this, "unpack") : null;
377        try {
378            if (m.getComposite() != m)
379                throw new ISOException ("Can't call packager on non Composite");
380
381            // if ISOMsg and headerLength defined
382            if (m instanceof ISOMsg && ((ISOMsg) m).getHeader()==null && headerLength>0)
383            {
384                byte[] h = new byte[headerLength];
385                in.read(h, 0, headerLength);
386                ((ISOMsg) m).setHeader(h);
387            }
388
389
390            if (!(fld[0] instanceof ISOMsgFieldPackager) &&
391                !(fld[0] instanceof ISOBitMapPackager))
392            {
393                ISOComponent mti = fld[0].createComponent(0);
394                fld[0].unpack(mti, in);
395                m.set (mti);
396            }
397
398            BitSet bmap = null;
399            int maxField = fld.length;
400            if (emitBitMap()) {
401                ISOBitMap bitmap = new ISOBitMap (-1);
402                getBitMapfieldPackager().unpack(bitmap, in);
403                bmap = (BitSet) bitmap.getValue();
404                if (evt != null)
405                    evt.addMessage ("<bitmap>"+bmap.toString()+"</bitmap>");
406                m.set (bitmap);
407                maxField = Math.min(maxField, bmap.size());
408            }
409
410            for (int i=getFirstField(); i<maxField; i++) {
411                if (bmap == null && fld[i] == null)
412                    continue;
413
414                if (bmap == null || bmap.get(i)) {
415                    if (fld[i] == null)
416                        throw new ISOException ("field packager '" + i + "' is null");
417
418                    ISOComponent c = fld[i].createComponent(i);
419                    fld[i].unpack (c, in);
420                    if (evt != null)
421                        fieldUnpackLogger(evt, i, c, fld[i], logFieldName);
422                    m.set(c);
423                }
424            }
425            if (bmap != null && bmap.get(65) && fld.length > 128 &&
426                fld[65] instanceof ISOBitMapPackager)
427            {
428                bmap= (BitSet) ((ISOComponent) m.getChildren().get(65)).getValue();
429                for (int i=1; i<64; i++) {
430                    if (bmap == null || bmap.get(i)) {
431                        ISOComponent c = fld[i+128].createComponent(i);
432                        fld[i+128].unpack (c, in);
433                        if (evt != null)
434                            fieldUnpackLogger(evt, i+128, c, fld[i+128], logFieldName);
435                        m.set(c);
436                    }
437                }
438            }
439        } catch (ISOException e) {
440            if (evt != null)
441                evt.addMessage (e);
442            throw e;
443        } catch (EOFException e) {
444            throw e;
445        } catch (Exception e) {
446            if (evt != null)
447                evt.addMessage (e);
448            throw new ISOException (e);
449        } finally {
450            if (evt != null)
451                Logger.log (evt);
452        }
453    }
454
455
456    /**
457     * Internal helper logging function.
458     * Assumes evt is not null.
459     *
460     * @param evt the log event to append messages to (must not be {@code null})
461     * @param fldno the field number being unpacked
462     * @param c the unpacked component carrying the field value
463     * @param fld the field packager that produced {@code c}
464     * @param logFieldName when {@code true}, emits the field description as an XML comment
465     * @throws ISOException if the value cannot be rendered for logging
466     */
467    protected static void fieldUnpackLogger(LogEvent evt, int fldno, ISOComponent c, ISOFieldPackager fld, boolean logFieldName) throws ISOException
468    {
469        evt.addMessage ("<unpack fld=\""+fldno
470            +"\" packager=\""+fld.getClass().getName()+ "\">");
471
472        if (logFieldName)
473            evt.addMessage("  <!-- "+fld.getDescription()+" -->");
474
475        if (c.getValue() instanceof ISOMsg)
476            evt.addMessage (c.getValue());
477        else if (c.getValue() instanceof byte[]) {
478            evt.addMessage ("  <value type='binary'>"
479                +ISOUtil.hexString((byte[]) c.getValue())
480                + "</value>");
481        }
482        else {
483            evt.addMessage ("  <value>"+c.getValue()+"</value>");
484        }
485        evt.addMessage ("</unpack>");
486    }
487
488
489    /**
490     * Returns the human-readable description of a field as defined by its packager.
491     *
492     * @param   m   the Container (i.e. an ISOMsg)
493     * @param   fldNumber the Field Number
494     * @return  Field Description
495     */
496    public String getFieldDescription(ISOComponent m, int fldNumber) {
497        return fld[fldNumber].getDescription();
498    }
499    /**
500     * Returns the field packager registered for the given field number.
501     *
502     * @param   fldNumber the Field Number
503     * @return  Field Packager for this field
504     */
505    public ISOFieldPackager getFieldPackager (int fldNumber) {
506        return fld != null && fldNumber < fld.length ? fld[fldNumber] : null;
507    }
508    /**
509     * Replaces the packager registered for a single field.
510     *
511     * @param   fldNumber the Field Number
512     * @param   fieldPackager the Field Packager
513     */
514    public void setFieldPackager
515        (int fldNumber, ISOFieldPackager fieldPackager)
516    {
517        fld[fldNumber] = fieldPackager;
518    }
519    /**
520     * Factory hook returning the {@link ISOMsg} subclass produced during unpack.
521     *
522     * @return a new {@link ISOMsg} instance
523     */
524    public ISOMsg createISOMsg () {
525        return new ISOMsg();
526    }
527    /**
528     * Returns the highest valid field number for this packager.
529     *
530     * @return 128 for ISO-8583, should return 64 for ANSI X9.2
531     */
532    protected int getMaxValidField() {
533        return 128;
534    }
535    /**
536     * Returns the field packager used for the primary bitmap.
537     *
538     * @return suitable ISOFieldPackager for Bitmap
539     */
540    protected ISOFieldPackager getBitMapfieldPackager() {
541        return fld[1];
542    }
543    public void setLogger (Logger logger, String realm) {
544        this.logger = logger;
545        this.realm  = realm;
546    }
547    public String getRealm () {
548        return realm;
549    }
550    public Logger getLogger() {
551        return logger;
552    }
553    /**
554     * Returns the configured ISO header length in bytes.
555     *
556     * @return number of leading bytes treated as the ISO header during unpack
557     */
558    public int getHeaderLength ()
559    {
560        return headerLength;
561    }
562    /**
563     * Sets the ISO header length in bytes.
564     *
565     * @param len number of leading bytes to treat as the ISO header during unpack
566     */
567    public void setHeaderLength(int len)
568    {
569        headerLength = len;
570    }
571    public String getDescription () {
572        return getClass().getName();
573    }
574}