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.iso.header.BaseHeader;
022import org.jpos.iso.packager.XMLPackager;
023import org.jpos.util.Loggeable;
024
025import java.io.*;
026import java.lang.ref.WeakReference;
027import java.util.*;
028
029/**
030 * implements <b>Composite</b>
031 * within a <b>Composite pattern</b>
032 *
033 * @author apr@cs.com.uy
034 * @version $Id$
035 * @see ISOComponent
036 * @see ISOField
037 */
038@SuppressWarnings("unchecked")
039public class ISOMsg extends ISOComponent
040    implements Cloneable, Loggeable, Externalizable
041{
042    protected Map<Integer,Object> fields;
043    protected int maxField;
044    protected ISOPackager packager;
045    protected boolean dirty, maxFieldDirty;
046    protected int direction;
047    protected ISOHeader header;
048    protected byte[] trailer;
049    protected int fieldNumber = -1;
050    public static final int INCOMING = 1;
051    public static final int OUTGOING = 2;
052    private static final long serialVersionUID = 4306251831901413975L;
053    private WeakReference sourceRef;
054
055    /**
056     * Creates an ISOMsg
057     */
058    public ISOMsg () {
059        fields = new TreeMap<>();
060        maxField = -1;
061        dirty = true;
062        maxFieldDirty=true;
063        direction = 0;
064        header = null;
065        trailer = null;
066    }
067    /**
068     * Creates a nested ISOMsg
069     * @param fieldNumber (in the outer ISOMsg) of this nested message
070     */
071    public ISOMsg (int fieldNumber) {
072        this();
073        setFieldNumber (fieldNumber);
074    }
075    /**
076     * changes this Component field number<br>
077     * Use with care, this method does not change
078     * any reference held by a Composite.
079     * @param fieldNumber new field number
080     */
081    @Override
082    public void setFieldNumber (int fieldNumber) {
083        this.fieldNumber = fieldNumber;
084    }
085    /**
086     * Creates an ISOMsg with given mti
087     * @param mti Msg's MTI
088     */
089    @SuppressWarnings("PMD.EmptyCatchBlock")
090    public ISOMsg (String mti) {
091        this();
092        try {
093            setMTI (mti);
094        } catch (ISOException ignored) {
095            // Should never happen as this is not an inner message
096        }
097    }
098    /**
099     * Sets the direction information related to this message
100     * @param direction can be either ISOMsg.INCOMING or ISOMsg.OUTGOING
101     */
102    public void setDirection(int direction) {
103        this.direction = direction;
104    }
105    /**
106     * Sets an optional message header image
107     * @param b header image
108     */
109    public void setHeader(byte[] b) {
110        header = new BaseHeader (b);
111    }
112
113    public void setHeader (ISOHeader header) {
114        this.header = header;
115    }
116    /**
117     * get optional message header image
118     * @return message header image (may be null)
119     */
120    public byte[] getHeader() {
121        return header != null ? header.pack() : null;
122    }
123
124    /**
125     * Sets optional trailer data.
126     * <p/>
127     * Note: The trailer data requires a customised channel that explicitly handles the trailer data from the ISOMsg.
128     *
129     * @param trailer The trailer data.
130     * @see BaseChannel#getMessageTrailer(ISOMsg).
131     * @see BaseChannel#sendMessageTrailer(ISOMsg, byte[]).
132     */
133    public void setTrailer(byte[] trailer) {
134        this.trailer = trailer;
135    }
136
137    /**
138     * Get optional trailer image.
139     *
140     * @return message trailer image (may be null)
141     */
142    public byte[] getTrailer() {
143        return this.trailer;
144    }
145
146    /**
147     * Return this messages ISOHeader
148     * @return header associated with this ISOMsg, can be null
149     */
150    public ISOHeader getISOHeader() {
151        return header;
152    }
153    /**
154     * @return the direction (ISOMsg.INCOMING or ISOMsg.OUTGOING)
155     * @see ISOChannel
156     */
157    public int getDirection() {
158        return direction;
159    }
160    /**
161     * @return true if this message is an incoming message
162     * @see ISOChannel
163     */
164    public boolean isIncoming() {
165        return direction == INCOMING;
166    }
167    /**
168     * @return true if this message is an outgoing message
169     * @see ISOChannel
170     */
171    public boolean isOutgoing() {
172        return direction == OUTGOING;
173    }
174    /**
175     * @return the max field number associated with this message
176     */
177    @Override
178    public int getMaxField() {
179        if (maxFieldDirty)
180            recalcMaxField();
181        return maxField;
182    }
183    private void recalcMaxField() {
184        maxField = 0;
185        for (Object obj : fields.keySet()) {
186            if (obj instanceof Integer)
187                maxField = Math.max(maxField, ((Integer) obj).intValue());
188        }
189        maxFieldDirty = false;
190    }
191    /**
192     * @param p - a peer packager
193     */
194    public void setPackager (ISOPackager p) {
195        packager = p;
196        if (packager == null) {
197            for (Object o : fields.values()) {
198                if (o instanceof ISOMsg)
199                    ((ISOMsg) o).setPackager(null);
200            }
201        }
202    }
203    /**
204     * @return the peer packager
205     */
206    public ISOPackager getPackager () {
207        return packager;
208    }
209    /**
210     * Set a field within this message
211     * @param c - a component
212     */
213    public void set (ISOComponent c) throws ISOException {
214        if (c != null) {
215            Integer i = (Integer) c.getKey();
216            fields.put (i, c);
217            if (i > maxField)
218                maxField = i;
219            dirty = true;
220        }
221    }
222
223    /**
224     * Creates an ISOField associated with fldno within this ISOMsg.
225     *
226     * @param fldno field number
227     * @param value field value
228     */
229    public void set(int fldno, String value) {
230        if (value == null) {
231            unset(fldno);
232            return;
233        }
234
235        try {
236            if (!(packager instanceof ISOBasePackager)) {
237                // No packager is available, we can't tell what the field
238                // might be, so treat as a String!
239                set(new ISOField(fldno, value));
240            }
241            else {
242                // This ISOMsg has a packager, so use it
243                Object obj = ((ISOBasePackager) packager).getFieldPackager(fldno);
244                if (obj instanceof ISOBinaryFieldPackager) {
245                    set(new ISOBinaryField(fldno, ISOUtil.hex2byte(value)));
246                } else {
247                    set(new ISOField(fldno, value));
248                }
249            }
250        } catch (ISOException ex) {}; //NOPMD: never happens for the given arguments of set methods
251    }
252
253    /**
254     * Creates an ISOField associated with fldno within this ISOMsg.
255     *
256     * @param fpath dot-separated field path (i.e. 63.2)
257     * @param value field value
258     */
259    public void set(String fpath, String value) {
260        StringTokenizer st = new StringTokenizer (fpath, ".");
261        ISOMsg m = this;
262        for (;;) {
263            int fldno = parseInt(st.nextToken());
264            if (st.hasMoreTokens()) {
265                Object obj = m.getValue(fldno);
266                if (obj instanceof ISOMsg)
267                    m = (ISOMsg) obj;
268                else
269                    /**
270                     * we need to go deeper, however, if the value == null then
271                     * there is nothing to do (unset) at the lower levels, so break now and save some processing.
272                     */
273                    if (value == null) {
274                        break;
275                    } else {
276                        try {
277                            // We have a value to set, so adding a level to hold it is sensible.
278                            m.set(m = new ISOMsg (fldno));
279                        } catch (ISOException ex) {} //NOPMD: never happens for the given arguments of set methods
280                    }
281            } else {
282                m.set(fldno, value);
283                break;
284            }
285        }
286    }
287
288    /**
289     * Creates an ISOField associated with fldno within this ISOMsg
290     * @param fpath dot-separated field path (i.e. 63.2)
291     * @param c component
292     * @throws ISOException on error
293     */
294    public void set(String fpath, ISOComponent c) throws ISOException {
295        StringTokenizer st = new StringTokenizer (fpath, ".");
296        ISOMsg m = this;
297        for (;;) {
298            int fldno = parseInt(st.nextToken());
299            if (st.hasMoreTokens()) {
300                Object obj = m.getValue(fldno);
301                if (obj instanceof ISOMsg)
302                    m = (ISOMsg) obj;
303                else
304                    /*
305                     * we need to go deeper, however, if the value == null then
306                     * there is nothing to do (unset) at the lower levels, so break now and save some processing.
307                     */
308                    if (c == null) {
309                        break;
310                    } else {
311                        // We have a value to set, so adding a level to hold it is sensible.
312                        m.set(m = new ISOMsg(fldno));
313                    }
314            } else {
315                if (c != null)
316                    c.setFieldNumber(fldno);
317                m.set(c);
318                break;
319            }
320        }
321    }
322    /**
323     * Creates an ISOField associated with fldno within this ISOMsg.
324     *
325     * @param fpath dot-separated field path (i.e. 63.2)
326     * @param value binary field value
327     */
328    public void set(String fpath, byte[] value) {
329        StringTokenizer st = new StringTokenizer (fpath, ".");
330        ISOMsg m = this;
331        for (;;) {
332            int fldno = parseInt(st.nextToken());
333            if (st.hasMoreTokens()) {
334                Object obj = m.getValue(fldno);
335                if (obj instanceof ISOMsg)
336                    m = (ISOMsg) obj;
337                else
338                    try {
339                        m.set(m = new ISOMsg (fldno));
340                    } catch (ISOException ex) {} //NOPMD: never happens for the given arguments of set methods
341            } else {
342                m.set(fldno, value);
343                break;
344            }
345        }
346    }
347
348    /**
349     * Creates an ISOBinaryField associated with fldno within this ISOMsg.
350     *
351     * @param fldno field number
352     * @param value field value
353     */
354    public void set(int fldno, byte[] value) {
355        if (value == null) {
356            unset(fldno);
357            return;
358        }
359
360        try {
361            set(new ISOBinaryField(fldno, value));
362        } catch (ISOException ex) {}; //NOPMD: never happens for the given arguments of set methods
363    }
364
365
366    /**
367     * Unset a field if it exists, otherwise ignore.
368     * @param fldno - the field number
369     */
370    @Override
371    public void unset (int fldno) {
372        if (fields.remove (fldno) != null)
373            dirty = maxFieldDirty = true;
374    }
375
376    /**
377     * Unsets several fields at once
378     * @param flds - array of fields to be unset from this ISOMsg
379     */
380    public void unset (int ... flds) {
381        for (int fld : flds)
382            unset(fld);
383    }
384
385    /**
386     * Unset a field referenced by a fpath if it exists, otherwise ignore.
387     *
388     * @param fpath dot-separated field path (i.e. 63.2)
389     */
390    public void unset(String fpath) {
391        StringTokenizer st = new StringTokenizer (fpath, ".");
392        ISOMsg m = this;
393        ISOMsg lastm = m;
394        int fldno = -1 ;
395        int lastfldno ;
396        for (;;) {
397            lastfldno = fldno;
398            fldno = parseInt(st.nextToken());
399            if (st.hasMoreTokens()) {
400                Object obj = m.getValue(fldno);
401                if (obj instanceof ISOMsg) {
402                    lastm = m;
403                    m = (ISOMsg) obj;
404                }
405                else {
406                    // No real way of unset further subfield, exit.
407                    break;
408                }
409            } else {
410                m.unset(fldno);
411                if (!m.hasFields() && lastfldno != -1) {
412                    lastm.unset(lastfldno);
413                }
414                break;
415            }
416        }
417    }
418
419    /**
420     * Unset a a set of fields referenced by fpaths if any ot them exist, otherwise ignore.
421     *
422     * @param fpaths dot-separated field paths (i.e. 63.2)
423     */
424    public void unset(String ... fpaths) {
425        for (String fpath : fpaths) {
426            unset(fpath);
427        }
428    }
429    /**
430     * In order to interchange <b>Composites</b> and <b>Leafs</b> we use
431     * getComposite(). A <b>Composite component</b> returns itself and
432     * a Leaf returns null.
433     *
434     * @return ISOComponent
435     */
436    @Override
437    public ISOComponent getComposite() {
438        return this;
439    }
440    /**
441     * setup BitMap
442     * @exception ISOException on error
443     */
444    public void recalcBitMap () throws ISOException {
445        if (!dirty)
446            return;
447
448        int mf = Math.min (getMaxField(), 192);
449
450        BitSet bmap = new BitSet (mf+62 >>6 <<6);
451        for (int i=1; i<=mf; i++)
452            if (fields.get (i) != null)
453                bmap.set (i);
454        set (new ISOBitMap (-1, bmap));
455        dirty = false;
456    }
457    /**
458     * clone fields
459     * @return copy of fields
460     */
461    @Override
462    public Map getChildren() {
463        return (Map) ((TreeMap)fields).clone();
464    }
465    /**
466     * pack the message with the current packager
467     * @return the packed message
468     * @exception ISOException
469     */
470    @Override
471    public byte[] pack() throws ISOException {
472        synchronized (this) {
473            recalcBitMap();
474            return packager.pack(this);
475        }
476    }
477    /**
478     * unpack a message
479     * @param b - raw message
480     * @return consumed bytes
481     * @exception ISOException
482     */
483    @Override
484    public int unpack(byte[] b) throws ISOException {
485        synchronized (this) {
486            return packager.unpack(this, b);
487        }
488    }
489    @Override
490    public void unpack (InputStream in) throws IOException, ISOException {
491        synchronized (this) {
492            packager.unpack(this, in);
493        }
494    }
495    /**
496     * dump the message to a PrintStream. The output is sorta
497     * XML, intended to be easily parsed.
498     * <br>
499     * Each component is responsible for its own dump function,
500     * ISOMsg just calls dump on every valid field.
501     * @param p - print stream
502     * @param indent - optional indent string
503     */
504    @Override
505    public void dump (PrintStream p, String indent) {
506        ISOComponent c;
507        p.print (indent + "<" + XMLPackager.ISOMSG_TAG);
508        switch (direction) {
509            case INCOMING:
510                p.print (" direction=\"incoming\"");
511                break;
512            case OUTGOING:
513                p.print (" direction=\"outgoing\"");
514                break;
515        }
516        if (fieldNumber != -1)
517            p.print (" "+XMLPackager.ID_ATTR +"=\""+fieldNumber +"\"");
518        p.println (">");
519        String newIndent = indent + "  ";
520        if (getPackager() != null) {
521           p.println (
522              newIndent
523           + "<!-- " + getPackager().getDescription() + " -->"
524           );
525        }
526        if (header instanceof Loggeable)
527            ((Loggeable) header).dump (p, newIndent);
528
529        for (int i : fields.keySet()) {
530           //If you want the bitmap dumped in the log, change the condition from (i >= 0) to (i >= -1). 
531            if (i >= 0) {
532                if ((c = (ISOComponent) fields.get(i)) != null)
533                    c.dump(p, newIndent);
534            }
535        }
536
537        p.println (indent + "</" + XMLPackager.ISOMSG_TAG+">");
538    }
539    /**
540     * get the component associated with the given field number
541     * @param fldno the Field Number
542     * @return the Component
543     */
544    public ISOComponent getComponent(int fldno) {
545        return (ISOComponent) fields.get(fldno);
546    }
547    /**
548     * Return the object value associated with the given field number
549     * @param fldno the Field Number
550     * @return the field Object
551     */
552    public Object getValue(int fldno) {
553        ISOComponent c = getComponent(fldno);
554        try {
555            return c != null ? c.getValue() : null;
556        } catch (ISOException ex) {
557            return null; //never happens for the given arguments of getValue method
558        }
559    }
560    /**
561     * Return the object value associated with the given field path
562     * @param fpath field path
563     * @return the field Object (may be null)
564     * @throws ISOException on error
565     */
566    public Object getValue (String fpath) throws ISOException {
567        StringTokenizer st = new StringTokenizer (fpath, ".");
568        ISOMsg m = this;
569        Object obj;
570        for (;;) {
571            int fldno = parseInt(st.nextToken());
572            obj = m.getValue (fldno);
573            if (obj==null){
574                // The user will always get a null value for an incorrect path or path not present in the message
575                // no point having the ISOException thrown for fields that were not received.
576                break;
577            }
578            if (st.hasMoreTokens()) {
579                if (obj instanceof ISOMsg) {
580                    m = (ISOMsg) obj;
581                }
582                else
583                    throw new ISOException ("Invalid path '" + fpath + "'");
584            } else
585                break;
586        }
587        return obj;
588    }
589    /**
590     * get the component associated with the given field number
591     * @param fpath field path
592     * @return the Component
593     * @throws ISOException on error
594     */
595    public ISOComponent getComponent (String fpath) throws ISOException {
596        StringTokenizer st = new StringTokenizer (fpath, ".");
597        ISOMsg m = this;
598        ISOComponent obj;
599        for (;;) {
600            int fldno = parseInt(st.nextToken());
601            obj = m.getComponent(fldno);
602            if (st.hasMoreTokens()) {
603                if (obj instanceof ISOMsg) {
604                    m = (ISOMsg) obj;
605                }
606                else
607                    break; // 'Quick' exit if hierarchy is not present.
608            } else
609                break;
610        }
611        return obj;
612    }
613    /**
614     * Return the String value associated with the given ISOField number
615     * @param fldno the Field Number
616     * @return field's String value
617     */
618    public String getString (int fldno) {
619        String s = null;
620        if (hasField (fldno)) {
621            Object obj = getValue(fldno);
622            if (obj instanceof String)
623                s = (String) obj;
624            else if (obj instanceof byte[])
625                s = ISOUtil.hexString((byte[]) obj);
626        }
627        return s;
628    }
629    /**
630     * Return the String value associated with the given field path
631     * @param fpath field path
632     * @return field's String value (may be null)
633     */
634    public String getString (String fpath) {
635        String s = null;
636        try {
637            Object obj = getValue(fpath);
638            if (obj instanceof String)
639                s = (String) obj;
640            else if (obj instanceof byte[])
641                s = ISOUtil.hexString ((byte[]) obj);
642        } catch (ISOException e) {
643            return null;
644        }
645        return s;
646    }
647    /**
648     * Return the byte[] value associated with the given ISOField number
649     * @param fldno the Field Number
650     * @return field's byte[] value or null if ISOException or UnsupportedEncodingException happens
651     */
652    public byte[] getBytes (int fldno) {
653        byte[] b = null;
654        if (hasField (fldno)) {
655            Object obj = getValue(fldno);
656            if (obj instanceof String)
657                b = ((String) obj).getBytes(ISOUtil.CHARSET);
658            else if (obj instanceof byte[])
659                b = (byte[]) obj;
660        }
661        return b;
662    }
663    /**
664     * Return the String value associated with the given field path
665     * @param fpath field path
666     * @return field's byte[] value (may be null)
667     */
668    public byte[] getBytes (String fpath) {
669        byte[] b = null;
670        try {
671            Object obj = getValue(fpath);
672            if (obj instanceof String)
673                b = ((String) obj).getBytes(ISOUtil.CHARSET);
674            else if (obj instanceof byte[])
675                b = (byte[]) obj;
676        } catch (ISOException ignored) {
677            return null;
678        }
679        return b;
680    }
681    /**
682     * Check if a given field is present
683     * @param fldno the Field Number
684     * @return boolean indicating the existence of the field
685     */
686    public boolean hasField(int fldno) {
687        return fields.get(fldno) != null;
688    }
689    /**
690     * Check if all fields are present
691     * @param fields an array of fields to check for presence
692     * @return true if all fields are present
693     */
694    public boolean hasFields (int[] fields) {
695        for (int field : fields)
696            if (!hasField(field))
697                return false;
698        return true;
699    }
700
701    /**
702     * Check if the message has any of these fields
703     * @param fields an array of fields to check for presence
704     * @return true if at least one field is present
705     */
706    public boolean hasAny (int[] fields) {
707        for (int field : fields)
708            if (hasField(field))
709                return true;
710        return false;
711    }
712    /**
713     * Check if the message has any of these fields
714     * @param fields to check for presence
715     * @return true if at least one field is present
716     */
717    public boolean hasAny (String... fields) {
718        for (String field : fields)
719            if (hasField (field))
720                return true;
721        return false;
722    }
723
724    /**
725     * Check if a field indicated by a fpath is present
726     * @param fpath dot-separated field path (i.e. 63.2)
727     * @return true if field present
728     */
729     public boolean hasField (String fpath) {
730         StringTokenizer st = new StringTokenizer (fpath, ".");
731         ISOMsg m = this;
732         for (;;) {
733             int fldno = parseInt(st.nextToken());
734             if (st.hasMoreTokens()) {
735                 Object obj = m.getValue(fldno);
736                 if (obj instanceof ISOMsg) {
737                     m = (ISOMsg) obj;
738                 }
739                 else {
740                     // No real way of checking for further subfields, return false, perhaps should be ISOException?
741                     return false;
742                 }
743             } else {
744                 return m.hasField(fldno);
745             }
746         }
747     }
748    /**
749     * @return true if ISOMsg has at least one field
750     */
751    public boolean hasFields () {
752        return !fields.isEmpty();
753    }
754    /**
755     * Don't call setValue on an ISOMsg. You'll sure get
756     * an ISOException. It's intended to be used on Leafs
757     * @param obj
758     * @throws org.jpos.iso.ISOException
759     * @see ISOField
760     * @see ISOException
761     */
762    @Override
763    public void setValue(Object obj) throws ISOException {
764        throw new ISOException ("setValue N/A in ISOMsg");
765    }
766
767    @Override
768    public Object clone() {
769        try {
770            ISOMsg m = (ISOMsg) super.clone();
771            m.fields = (TreeMap) ((TreeMap) fields).clone();
772            if (header != null)
773                m.header = (ISOHeader) header.clone();
774            if (trailer != null)
775                m.trailer = trailer.clone();
776            for (Integer k : fields.keySet()) {
777                ISOComponent c = (ISOComponent) m.fields.get(k);
778                if (c instanceof ISOMsg)
779                    m.fields.put(k, ((ISOMsg) c).clone());
780            }
781            return m;
782        } catch (CloneNotSupportedException e) {
783            throw new InternalError();
784        }
785    }
786
787    /**
788     * Partially clone an ISOMsg
789     * @param fields int array of fields to go
790     * @return new ISOMsg instance
791     */
792    @SuppressWarnings("PMD.EmptyCatchBlock")
793    public Object clone(int ... fields) {
794        try {
795            ISOMsg m = (ISOMsg) super.clone();
796            m.fields = new TreeMap();
797            for (int field : fields) {
798                if (hasField(field)) {
799                    try {
800                        ISOComponent c = getComponent(field);
801                        if (c instanceof ISOMsg) {
802                            m.set((ISOMsg)((ISOMsg)c).clone());
803                        } else {
804                            m.set(c);
805                        }
806                    } catch (ISOException ignored) {
807                        // should never happen
808                    }
809                }
810            }
811            return m;
812        } catch (CloneNotSupportedException e) {
813            throw new InternalError();
814        }
815    }
816
817    /**
818     * Partially clone an ISOMsg by field paths
819     * @param fpaths string array of field paths to copy
820     * @return new ISOMsg instance
821     */
822    public ISOMsg clone(String ... fpaths) {
823        try {
824            ISOMsg m = (ISOMsg) super.clone();
825            m.fields = new TreeMap();
826            for (String fpath : fpaths) {
827                try {
828                    ISOComponent component = getComponent(fpath);
829                    if (component instanceof ISOMsg) {
830                        m.set(fpath, (ISOMsg)((ISOMsg)component).clone());
831                    } else if (component != null) {
832                        m.set(fpath, component);
833                    }
834                } catch (ISOException ignored) {
835                    //should never happen
836                }
837            }
838            return m;
839        } catch (CloneNotSupportedException e) {
840            throw new InternalError();
841        }
842    }
843
844    /**
845     * Merges the content of the specified ISOMsg into this ISOMsg instance.
846     * It iterates over the fields of the input message and, for each field that is present,
847     * sets the corresponding component in this message to the value from the input message.
848     * This operation includes all fields that are present in the input message, but does not remove
849     * any existing fields from this message unless they are explicitly overwritten by the input message.
850     * <p>
851     * If the input message contains a header (non-null), this method also clones the header
852     * and sets it as the header of this message.
853     *
854     * @param m The ISOMsg to merge into this ISOMsg. It must not be {@code null}.
855     *          The method does nothing if {@code m} is {@code null}.
856     * @param mergeHeader A boolean flag indicating whether to merge the header of the input message into this message.
857     *
858     */
859    @SuppressWarnings("PMD.EmptyCatchBlock")
860    public void merge (ISOMsg m, boolean mergeHeader) {
861        for (int i : m.fields.keySet()) {
862            try {
863                if (i >= 0 && m.hasField(i))
864                    set(m.getComponent(i));
865            } catch (ISOException ignored) {
866                // should never happen
867            }
868        }
869        if (mergeHeader && m.header != null)
870            header = (ISOHeader) m.header.clone();
871    }
872
873    /*
874     * Merges the content of the specified ISOMsg into this ISOMsg instance, excluding the header.
875     * This method is a convenience wrapper around {@link #merge(ISOMsg, boolean)} with the {@code mergeHeader}
876     * parameter set to {@code false} for backward compatibility, indicating that the header of the input message
877     * will not be merged.
878     */
879    public void merge (ISOMsg m) {
880        merge (m, false);
881    }
882
883    /**
884     * @return a string suitable for a log
885     */
886    @Override
887    public String toString() {
888        StringBuilder s = new StringBuilder();
889        if (isIncoming())
890            s.append(" In: ");
891        else if (isOutgoing())
892            s.append("Out: ");
893        else
894            s.append("     ");
895
896        s.append(getString(0));
897        if (hasField(11)) {
898            s.append(' ');
899            s.append(getString(11));
900        }
901        if (hasField(41)) {
902            s.append(' ');
903            s.append(getString(41));
904        }
905        return s.toString();
906    }
907    @Override
908    public Object getKey() throws ISOException {
909        if (fieldNumber != -1)
910            return fieldNumber;
911        throw new ISOException ("This is not a subField");
912    }
913    @Override
914    public Object getValue() {
915        return this;
916    }
917    /**
918     * @return true on inner messages
919     */
920    public boolean isInner() {
921        return fieldNumber > -1;
922    }
923    /**
924     * @param mti new MTI
925     * @exception ISOException if message is inner message
926     */
927    public void setMTI (String mti) throws ISOException {
928        if (isInner())
929            throw new ISOException ("can't setMTI on inner message");
930        set (new ISOField (0, mti));
931    }
932    /**
933     * moves a field (renumber)
934     * @param oldFieldNumber old field number
935     * @param newFieldNumber new field number
936     * @throws ISOException on error
937     */
938    public void move (int oldFieldNumber, int newFieldNumber)
939        throws ISOException
940    {
941        ISOComponent c = getComponent (oldFieldNumber);
942        unset (oldFieldNumber);
943        if (c != null) {
944            c.setFieldNumber (newFieldNumber);
945            set (c);
946        } else
947            unset (newFieldNumber);
948    }
949
950    @Override
951    public int getFieldNumber () {
952        return fieldNumber;
953    }
954
955    /**
956     * @return true is message has MTI field
957     * @exception ISOException if this is an inner message
958     */
959    public boolean hasMTI() throws ISOException {
960        if (isInner())
961            throw new ISOException ("can't hasMTI on inner message");
962        else
963            return hasField(0);
964    }
965    /**
966     * @return current MTI
967     * @exception ISOException on inner message or MTI not set
968     */
969    public String getMTI() throws ISOException {
970        if (isInner())
971            throw new ISOException ("can't getMTI on inner message");
972        else if (!hasField(0))
973            throw new ISOException ("MTI not available");
974        return (String) getValue(0);
975    }
976
977    /**
978     * @return true if message "seems to be" a request
979     * @exception ISOException on MTI not set
980     */
981    public boolean isRequest() throws ISOException {
982        return Character.getNumericValue(getMTI().charAt (2))%2 == 0;
983    }
984    /**
985     * @return true if message "seems not to be" a request
986     * @exception ISOException on MTI not set
987     */
988    public boolean isResponse() throws ISOException {
989        return !isRequest();
990    }
991    /**
992     * @return true if message class is "authorization"
993     * @exception ISOException on MTI not set
994     */
995    public boolean isAuthorization() throws ISOException {
996        return hasMTI() && getMTI().charAt(1) == '1';
997    }
998    /**
999     * @return true if message class is "financial"
1000     * @exception ISOException on MTI not set
1001     */
1002    public boolean isFinancial() throws ISOException {
1003        return hasMTI() && getMTI().charAt(1) == '2';
1004    }
1005    /**
1006     * @return true if message class is "file action"
1007     * @exception ISOException on MTI not set
1008     */
1009    public boolean isFileAction() throws ISOException {
1010        return hasMTI() && getMTI().charAt(1) == '3';
1011    }
1012    /**
1013     * @return true if message class is "reversal"
1014     * @exception ISOException on MTI not set
1015     */
1016    public boolean isReversal() throws ISOException {
1017        return hasMTI() && getMTI().charAt(1) == '4' && (getMTI().charAt(3) == '0' || getMTI().charAt(3) == '1');
1018    }
1019    /**
1020     * @return true if message class is "chargeback"
1021     * @exception ISOException on MTI not set
1022     */
1023    public boolean isChargeback() throws ISOException {
1024        return hasMTI() && getMTI().charAt(1) == '4' && (getMTI().charAt(3) == '2' || getMTI().charAt(3) == '3');
1025    }
1026    /**
1027     * @return true if message class is "reconciliation"
1028     * @exception ISOException on MTI not set
1029     */
1030    public boolean isReconciliation() throws ISOException {
1031        return hasMTI() && getMTI().charAt(1) == '5';
1032    }
1033    /**
1034     * @return true if message class is "administrative"
1035     * @exception ISOException on MTI not set
1036     */
1037    public boolean isAdministrative() throws ISOException {
1038        return hasMTI() && getMTI().charAt(1) == '6';
1039    }
1040    /**
1041     * @return true if message class is "fee collection"
1042     * @exception ISOException on MTI not set
1043     */
1044    public boolean isFeeCollection() throws ISOException {
1045        return hasMTI() && getMTI().charAt(1) == '7';
1046    }
1047    /**
1048     * @return true if message class is "fee collection"
1049     * @exception ISOException on MTI not set
1050     */
1051    public boolean isNetworkManagement() throws ISOException {
1052        return hasMTI() && getMTI().charAt(1) == '8';
1053    }
1054    /**
1055     * @return true if message is Retransmission
1056     * @exception ISOException on MTI not set
1057     */
1058    public boolean isRetransmission() throws ISOException {
1059        return getMTI().charAt(3) == '1';
1060    }
1061    /**
1062     * sets an appropriate response MTI.
1063     *
1064     * i.e. 0100 becomes 0110<br>
1065     * i.e. 0201 becomes 0210<br>
1066     * i.e. 1201 becomes 1210<br>
1067     * @exception ISOException on MTI not set or it is not a request
1068     */
1069    public void setResponseMTI() throws ISOException {
1070        if (!isRequest())
1071            throw new ISOException ("not a request - can't set response MTI");
1072
1073        String mti = getMTI();
1074        char c1 = mti.charAt(3);
1075        char c2 = '0';
1076        switch (c1)
1077        {
1078            case '0' :
1079            case '1' : c2='0';break;
1080            case '2' :
1081            case '3' : c2='2';break;
1082            case '4' :
1083            case '5' : c2='4';break;
1084
1085        }
1086        set (new ISOField (0,
1087            mti.substring(0,2)
1088            +(Character.getNumericValue(getMTI().charAt (2))+1) + c2
1089            )
1090        );
1091    }
1092    /**
1093     * sets an appropriate retransmission MTI<br>
1094     * @exception ISOException on MTI not set or it is not a request
1095     */
1096    public void setRetransmissionMTI() throws ISOException {
1097        if (!isRequest())
1098            throw new ISOException ("not a request");
1099
1100        set (new ISOField (0, getMTI().substring(0,3) + "1"));
1101    }
1102    protected void writeHeader (ObjectOutput out) throws IOException {
1103        int len = header.getLength();
1104        if (len > 0) {
1105            out.writeByte ('H');
1106            out.writeShort (len);
1107            out.write (header.pack());
1108        }
1109    }
1110
1111    protected void readHeader (ObjectInput in)
1112        throws IOException, ClassNotFoundException
1113    {
1114        byte[] b = new byte[in.readShort()];
1115        in.readFully (b);
1116        setHeader (b);
1117    }
1118    protected void writePackager(ObjectOutput out) throws IOException {
1119        out.writeByte('P');
1120        String pclass = packager.getClass().getName();
1121        byte[] b = pclass.getBytes();
1122        out.writeShort(b.length);
1123        out.write(b);
1124    }
1125    protected void readPackager(ObjectInput in) throws IOException,
1126    ClassNotFoundException {
1127        byte[] b = new byte[in.readShort()];
1128        in.readFully(b);
1129        try {
1130            Class mypClass = Class.forName(new String(b));
1131            ISOPackager myp = (ISOPackager) mypClass.newInstance();
1132            setPackager(myp);
1133        } catch (Exception e) {
1134            setPackager(null);
1135        }
1136
1137}
1138    protected void writeDirection (ObjectOutput out) throws IOException {
1139        out.writeByte ('D');
1140        out.writeByte (direction);
1141    }
1142    protected void readDirection (ObjectInput in)
1143        throws IOException, ClassNotFoundException
1144    {
1145        direction = in.readByte();
1146    }
1147
1148    @Override
1149    public void writeExternal (ObjectOutput out) throws IOException {
1150        out.writeByte (0);  // reserved for future expansion (version id)
1151        out.writeShort (fieldNumber);
1152
1153        if (header != null)
1154            writeHeader (out);
1155        if (packager != null)
1156            writePackager(out);
1157        if (direction > 0)
1158            writeDirection (out);
1159
1160        // List keySet = new ArrayList (fields.keySet());
1161        // Collections.sort (keySet);
1162        for (Object o : fields.values()) {
1163            ISOComponent c = (ISOComponent) o;
1164            if (c instanceof ISOMsg) {
1165                writeExternal(out, 'M', c);
1166            } else if (c instanceof ISOBinaryField) {
1167                writeExternal(out, 'B', c);
1168            } else if (c instanceof ISOAmount) {
1169                writeExternal(out, 'A', c);
1170            } else if (c instanceof ISOField) {
1171                writeExternal(out, 'F', c);
1172            }
1173        }
1174        out.writeByte ('E');
1175    }
1176
1177    @Override
1178    public void readExternal  (ObjectInput in)
1179        throws IOException, ClassNotFoundException
1180    {
1181        in.readByte();  // ignore version for now
1182        fieldNumber = in.readShort();
1183        byte fieldType;
1184        ISOComponent c;
1185        try {
1186            while ((fieldType = in.readByte()) != 'E') {
1187                c = null;
1188                switch (fieldType) {
1189                    case 'F':
1190                        c = new ISOField ();
1191                        break;
1192                    case 'A':
1193                        c = new ISOAmount ();
1194                        break;
1195                    case 'B':
1196                        c = new ISOBinaryField ();
1197                        break;
1198                    case 'M':
1199                        c = new ISOMsg ();
1200                        break;
1201                    case 'H':
1202                        readHeader (in);
1203                        break;
1204                    case 'P':
1205                        readPackager(in);
1206                        break;
1207                    case 'D':
1208                        readDirection (in);
1209                        break;
1210                    default:
1211                        throw new IOException ("malformed ISOMsg");
1212                }
1213                if (c != null) {
1214                    ((Externalizable)c).readExternal (in);
1215                    set (c);
1216                }
1217            }
1218        }
1219        catch (ISOException e) {
1220            throw new IOException (e.getMessage());
1221        }
1222    }
1223    /**
1224     * Let this ISOMsg object hold a weak reference to an ISOSource
1225     * (usually used to carry a reference to the incoming ISOChannel)
1226     * @param source an ISOSource
1227     */
1228    public void setSource (ISOSource source) {
1229        this.sourceRef = new WeakReference (source);
1230    }
1231    /**
1232     * @return an ISOSource or null
1233     */
1234    public ISOSource getSource () {
1235        return sourceRef != null ? (ISOSource) sourceRef.get () : null;
1236    }
1237    private void writeExternal (ObjectOutput out, char b, ISOComponent c) throws IOException {
1238        out.writeByte (b);
1239        ((Externalizable) c).writeExternal (out);
1240    }
1241    private int parseInt (String s) {
1242        return s.startsWith("0x") ? Integer.parseInt(s.substring(2), 16) : Integer.parseInt(s);
1243    }
1244}
1245