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.util;
020
021import java.io.ByteArrayInputStream;
022import java.io.EOFException;
023import java.io.FileNotFoundException;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.InputStreamReader;
027import java.io.PrintStream;
028import java.net.URL;
029import java.nio.charset.Charset;
030import java.util.Arrays;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.LinkedHashMap;
035import java.util.Map;
036import java.util.Map.Entry;
037import java.util.Objects;
038import java.util.Set;
039
040import org.jdom2.Element;
041import org.jdom2.JDOMException;
042import org.jdom2.input.SAXBuilder;
043import org.jpos.iso.ISOException;
044import org.jpos.iso.ISOUtil;
045import org.jpos.space.Space;
046import org.jpos.space.SpaceFactory;
047
048/**
049 * General purpose, Field Separator delimited message.
050 *
051 * <h1>How to use</h1>
052 * <p>
053 * The message format (or schema) is defined in xml files containing a schema element, with an optional id attribute, and multiple
054 * field elements. A field element is made up of the following attributes:
055 * <dl>
056 * <dt>id</dt>
057 * <dd>The name of the field. This is used in calls to {@link FSDMsg#set(String, String)}. It should be unique amongst the fields in an FSDMsg.</dd>
058 * <dt>length</dt>
059 * <dd>The maximum length of the data allowed in this field. Fixed length fields will be padded to this length. A zero length is allowed, and can
060 * be useful to define extra separator characters in the message.</dd>
061 * <dt>type</dt>
062 * <dd>The type of the included data, including an optional separator for marking the end of the field and the beginning of the next one. The data type
063 * is defined by the first char of the type, and the separator is defined by the following chars. If a field separator is specified, then no
064 * padding is done on values for this field.
065 * </dd>
066 * <dt>key</dt>
067 * <dd>If this optional attribute has a value of "true", then fields from another schema, specified by the value, are appended to this schema.</dd>
068 * <dt>separator</dt>
069 * <dd>An optional attribute containing the separator for the field. This is the preferred method of specifying the separator. See the list of optional</dd>
070 * </dl>
071 * <p>
072 * Possible types are:
073 * <dl>
074 * <dt>A</dt><dd>Alphanumeric. Padding if any is done with spaces to the right.</dd>
075 * <dt>B</dt><dd>Binary. Padding, if any, is done with zeros to the left.</dd>
076 * <dt>K</dt><dd>Constant. The value is specified by the field content. No padding is done.</dd>
077 * <dt>N</dt><dd>Numeric. Padding, if any, is done with zeros to the left.</dd>
078 * </dl>
079 * </p>
080 * <p>
081 * Supported field separators are:
082 * <dl>
083 * <dt>FS</dt><dd>Field separator using '034' as the separator.</dd>
084 * <dt>US</dt><dd>Field separator using '037' as the separator.</dd>
085 * <dt>GS</dt><dd>Group separator using '035' as the separator.</dd>
086 * <dt>RS</dt><dd>Row separator using '036' as the separator.</dd>
087 * <dt>PIPE</dt><dd>Field separator using '|' as the separator.</dd>
088 * <dt>EOF</dt><dd>End of File - no separator character is emitted, but also no padding is done. Also if the end of file is reached
089 * parsing a message, then no exception is thrown.</dd>
090 * <dt>DS</dt><dd>A dummy separator. This is similar to EOF, but the message stream must not end before it is allowed.</dd>
091 * <dt>EOM</dt><dd>End of message separator. This reads all bytes available in the stream.
092 * </dl>
093 * </p>
094 * <p>
095 * Key fields allow you to specify a tree of possible message formats. The key fields are the fork points of the tree.
096 * Multiple key fields are supported. It is also possible to have more key fields specified in appended schemas.
097 * </p>
098 *
099 * @author Alejandro Revila
100 * @author Mark Salter
101 * @author Dave Bergert
102 * @since 1.4.7
103 */
104@SuppressWarnings("unchecked")
105public class FSDMsg implements Loggeable, Cloneable {
106    public static char FS = '\034';
107    public static char US = '\037';
108    public static char GS = '\035';
109    public static char RS = '\036';
110    public static char EOF = '\000';
111    public static char PIPE = '\u007C';
112    public static char EOM = '\000';
113
114    private static final Set<String> DUMMY_SEPARATORS = new HashSet<>(Arrays.asList("DS", "EOM"));
115    private static final String EOM_SEPARATOR = "EOM";
116    private static final int READ_BUFFER = 8192;
117
118    Map<String,String> fields;
119    Map<String, Character> separators;
120
121    String baseSchema;
122    String basePath;
123    byte[] header;
124    Charset charset;
125    private int readCount;
126
127    /**
128     * Creates a FSDMsg with a specific base path for the message format schema.
129     * @param basePath   schema path, for example: "file:src/data/NDC-" looks for a file src/data/NDC-base.xml
130     */
131    public FSDMsg (String basePath) {
132        this (basePath, "base");
133    }
134
135    /**
136     * Creates a FSDMsg with a specific base path for the message format schema, and a base schema name. For instance,
137     * FSDMsg("file:src/data/NDC-", "root") will look for a file: src/data/NDC-root.xml
138     * @param basePath   schema path
139     * @param baseSchema schema name
140     */
141    public FSDMsg (String basePath, String baseSchema) {
142        super();
143        fields = new LinkedHashMap<>();
144        separators = new LinkedHashMap<>();
145        this.basePath   = basePath;
146        this.baseSchema = baseSchema;
147        charset = ISOUtil.CHARSET;
148        readCount = 0;
149
150        setSeparator("FS", FS);
151        setSeparator("US", US);
152        setSeparator("GS", GS);
153        setSeparator("RS", RS);
154        setSeparator("EOF", EOF);
155        setSeparator("PIPE", PIPE);
156    }
157    public String getBasePath() {
158        return basePath;
159    }
160    public String getBaseSchema() {
161        return baseSchema;
162    }
163
164    public void setCharset(Charset charset) {
165        this.charset = charset;
166    }
167
168    /*
169     * add a new or override an existing separator type/char pair.
170     *
171     *  @param separatorName   string of type used in definition (FS, US etc)
172     *  @param separator       char representing type
173     */
174    public void setSeparator(String separatorName, char separator) {
175        separators.put(separatorName, separator);
176    }
177
178    /*
179     * add a new or override an existing separator type/char pair.
180     *
181     *  @param separatorName   string of type used in definition (FS, US etc)
182     *  @param separator       char representing type
183     */
184    public void unsetSeparator(String separatorName) {
185        if (!separators.containsKey(separatorName))
186            throw new IllegalArgumentException("unsetSeparator was attempted for "+
187                      separatorName+" which was not previously defined.");
188
189        separators.remove(separatorName);
190    }
191
192    /**
193     * parse message. If the stream ends before the message is completely read, then the method adds an EOF field.
194     *
195     * @param is input stream
196     *
197     * @throws IOException
198     * @throws JDOMException
199     */
200    public void unpack (InputStream is)
201        throws IOException, JDOMException {
202        try {
203            if (is.markSupported())
204                is.mark(READ_BUFFER);
205            unpack (new InputStreamReader(is, charset), getSchema (baseSchema));
206            if (is.markSupported()) {
207                is.reset();
208                is.skip (readCount);
209                readCount = 0;
210            }
211        } catch (EOFException e) {
212            if (!fields.isEmpty())
213                fields.put ("EOF", "true");         // some fields were read, but unexpected EOF found
214            else                                    // nothing new since last msg, fields were read; no more msgs from this stream
215                throw e;                            // just rethrow the exception
216        }
217    }
218    /**
219     * parse message. If the stream ends before the message is completely read, then the method adds an EOF field.
220     *
221     * @param b message image
222     *
223     * @throws IOException
224     * @throws JDOMException
225     */
226    public void unpack (byte[] b)
227        throws IOException, JDOMException {
228        unpack (new ByteArrayInputStream (b));
229    }
230
231    /**
232     * @return message string
233     * @throws org.jdom2.JDOMException
234     * @throws java.io.IOException
235     * @throws ISOException
236     */
237    public String pack ()
238        throws JDOMException, IOException, ISOException
239    {
240        StringBuilder sb = new StringBuilder ();
241        pack (getSchema (baseSchema), sb);
242        return sb.toString ();
243    }
244    public byte[] packToBytes ()
245        throws JDOMException, IOException, ISOException
246    {
247        return pack().getBytes(charset);
248    }
249
250            protected String get (String id, String type, int length, String defValue, String separator)
251        throws ISOException
252    {
253            return get(id,type,length,defValue,separator,true);
254    }
255    protected String get (String id, String type, int length, String defValue, String separator, boolean unPad)
256        throws ISOException
257    {
258        String value = fields.get (id);
259        if (value == null)
260            value = defValue == null ? "" : defValue;
261
262        type = type.toUpperCase ();
263        int lengthLength = 0;
264        while (type.charAt(0) == 'L') {
265            lengthLength++;
266            type = type.substring(1);
267        }
268
269        switch (type.charAt (0)) {
270            case 'N':
271                if (!isSeparated(separator)) {
272                    value = ISOUtil.zeropad (value, length);
273                } // else Leave value unpadded.
274                break;
275            case 'A':
276                if (!isSeparated(separator) && lengthLength == 0) {
277                    value = ISOUtil.strpad (value, length);
278                } // else Leave value unpadded.
279                if (value.length() > length)
280                    value = value.substring(0,length);
281                break;
282            case 'K':
283                if (defValue != null)
284                    value = defValue;
285                break;
286            case 'B':
287                if (length << 1 < value.length())
288                    throw new IllegalArgumentException("field content=" + value
289                            + " is too long to fit in field " + id
290                            + " whose length is " + length);
291
292                if (isSeparated(separator)) {
293                    // Convert but do not pad if this field ends with a
294                    // separator
295                    value = new String(ISOUtil.hex2byte(value), charset);
296                } else {
297                    value = new String(ISOUtil.hex2byte(ISOUtil.zeropad(
298                            value, length << 1).substring(0, length << 1)), charset);
299                }
300                break;
301        }
302
303        if (lengthLength == 0 && (!isSeparated(separator) || isBinary(type) || EOM_SEPARATOR.equals(separator) ||(isSeparated(separator) && !unPad)))
304          return value;
305        else {
306            if (lengthLength > 0) {
307                String format = String.format("%%0%dd%%s", lengthLength);
308                value = String.format(format, value.length(), value);
309            } else {
310                value = ISOUtil.blankUnPad(value);
311            }
312        }
313        return value;
314    }
315
316    private boolean isSeparated(String separator) {
317        /*
318         * if type's last two characters appear in our Map of separators,
319         * return true
320         */
321        if (separator == null)
322            return false;
323        else if (separators.containsKey (separator))
324            return true;
325        else if (isDummySeparator (separator))
326            return true;
327        else
328            try {
329                if (Character.isDefined(Integer.parseInt(separator,16))) {
330                    setSeparator(separator, (char)Long.parseLong(separator,16));
331                    return true;
332                }
333            } catch (NumberFormatException ignored) {
334                throw new IllegalArgumentException("Invalid separator '"+ separator + "'");
335            }
336        throw new IllegalArgumentException("isSeparated called on separator="+
337                      separator+" which was not previously defined.");
338    }
339
340    private boolean isDummySeparator(String separator) {
341        return DUMMY_SEPARATORS.contains(separator);
342    }
343
344    private boolean isBinary(String type) {
345        /*
346         * if type's first digit is a 'B' return true
347         */
348        return type.startsWith("B");
349    }
350
351    public boolean isSeparator(byte b) {
352        return separators.containsValue((char) b);
353    }
354
355    private String getSeparatorType(String type) {
356        if (type.length() > 2 && !(type.charAt(0) == 'L')) {
357            return type.substring(1);
358        }
359        return null;
360    }
361
362    private char getSeparator(String separator) {
363        if (separators.containsKey(separator))
364            return separators.get(separator);
365        else if (isDummySeparator (separator)) {
366            // Dummy separator type, return 0 to indicate nothing to add.
367            return 0;
368        }
369
370        throw new IllegalArgumentException("getSeparator called on separator="+
371                      separator+" which was not previously defined.");
372    }
373
374    protected void pack (Element schema, StringBuilder sb)
375        throws JDOMException, IOException, ISOException
376    {
377        String keyOff = "";
378        String defaultKey = "";
379        for (Element elem : schema.getChildren("field")) {
380            String id    = elem.getAttributeValue ("id");
381            int length   = Integer.parseInt (elem.getAttributeValue ("length"));
382            String type  = elem.getAttributeValue ("type");
383            // For backward compatibility, look for a separator at the end of the type attribute, if no separator has been defined.
384            String separator = elem.getAttributeValue ("separator");
385            if (type != null && separator == null) {
386                separator = getSeparatorType (type);
387            }
388            boolean unPad = true;
389            if (separator != null && elem.getAttributeValue("pack_unpad") != null) {
390                unPad = Boolean.valueOf(elem.getAttributeValue("pack_unpad"));
391            }
392            boolean key  = "true".equals (elem.getAttributeValue ("key"));
393            Map properties = key ? loadProperties(elem) : Collections.EMPTY_MAP;
394            String defValue = elem.getText();
395            // If properties were specified, then the defValue contains lots of \n and \t in it. It should just be set to the empty string, or null.
396            if (!properties.isEmpty()) {
397                defValue = defValue.replace("\n", "").replace("\t", "").replace("\r", "");
398            }
399            String value = get (id, type, length, defValue, separator, unPad);
400            sb.append (value);
401
402            if (isSeparated(separator)) {
403                char c = getSeparator(separator);
404                if (c > 0)
405                    sb.append(c);
406            }
407            if (key) {
408                String v = isBinary(type) ? ISOUtil.hexString(value.getBytes(charset)) : value;
409                keyOff = keyOff + normalizeKeyValue(v, properties);
410                defaultKey += elem.getAttributeValue ("default-key");
411            }
412        }
413        if (keyOff.length() > 0)
414            pack (getSchema (getId (schema), keyOff, defaultKey), sb);
415    }
416
417    private Map loadProperties(Element elem) {
418        Map props = new HashMap ();
419        for (Element prop : elem.getChildren ("property")) {
420                String name = prop.getAttributeValue ("name");
421                String value = prop.getAttributeValue ("value");
422                props.put (name, value);
423        }
424            return props;
425    }
426
427          private String normalizeKeyValue(String value, Map<?,String> properties) {
428        if (properties.containsKey(value)) {
429            return properties.get(value);
430        }
431        return ISOUtil.normalize(value);
432    }
433
434    protected void unpack (InputStreamReader r, Element schema)
435        throws IOException, JDOMException {
436
437        String keyOff = "";
438        String defaultKey = "";
439        for (Element elem : schema.getChildren("field")) {
440            String id    = elem.getAttributeValue ("id");
441            int length   = Integer.parseInt (elem.getAttributeValue ("length"));
442            String type  = elem.getAttributeValue ("type").toUpperCase();
443            String separator = elem.getAttributeValue ("separator");
444            if (/* type != null && */       // can't be null or we would have NPE'ed when .toUpperCase()
445                separator == null) {
446                separator = getSeparatorType (type);
447            }
448            boolean key  = "true".equals (elem.getAttributeValue ("key"));
449            Map properties = key ? loadProperties(elem) : Collections.EMPTY_MAP;
450
451            String value = readField(r, id, length, type, separator);
452
453            if (key) {
454                keyOff = keyOff + normalizeKeyValue(value, properties);
455                defaultKey += elem.getAttributeValue ("default-key");
456            }
457
458            // constant fields should have read the constant value
459            if ("K".equals(type) && !value.equals (elem.getText()))
460                throw new IllegalArgumentException (
461                    "Field "+id
462                       + " value='"     +value
463                       + "' expected='" + elem.getText () + "'"
464                );
465        }
466        if (keyOff.length() > 0) {
467            unpack(r, getSchema (getId (schema), keyOff, defaultKey));      // recursion
468        }
469    }
470    private String getId (Element e) {
471        String s = e.getAttributeValue ("id");
472        return s == null ? "" : s;
473    }
474    protected String read (InputStreamReader r, int len, String type, String separator)
475        throws IOException
476    {
477        StringBuilder sb = new StringBuilder();
478        char[] c = new char[1];
479        boolean expectSeparator = isSeparated(separator);
480        boolean separated = expectSeparator;
481        char separatorChar= expectSeparator ? getSeparator(separator) : '\0';
482
483        if (EOM_SEPARATOR.equals(separator)) {
484            // Grab what's left.
485            char[] rest = new char[32];
486            int con;
487            while ((con = r.read(rest, 0, rest.length)) >= 0) {
488                readCount += con;
489                if (rest.length == con)
490                    sb.append(rest);
491                else
492                    sb.append(Arrays.copyOf(rest, con));
493            }
494        } else if (isDummySeparator(separator)) {
495            /*
496             * No need to look for a separator, that is not there! Try and take
497             * len bytes from the stream.
498             */
499            for (int i = 0; i < len; i++) {
500                if (r.read(c) < 0) {
501                    break; // end of stream indicates end of field?
502                }
503                readCount++;
504                sb.append(c[0]);
505            }
506        } else {
507            int lengthLength = 0;
508            if (type != null && type.startsWith("L")) {
509                while (type.charAt(0) == 'L') {
510                    lengthLength++;
511                    type = type.substring(1);
512                }
513                if (lengthLength > 0) {
514                    char[] ll = new char[lengthLength];
515                    if (r.read(ll) != lengthLength)
516                        throw new EOFException();
517                    len = Integer.parseInt(new String(ll));
518                }
519            }
520            for (int i = 0; i < len; i++) {
521                if (r.read(c) < 0) {
522                    if (!"EOF".equals(separator))
523                        throw new EOFException();
524                    else {
525                        separated = false;
526                        break;
527                    }
528                }
529                readCount++;
530                if (expectSeparator && c[0] == separatorChar) {
531                    separated = false;
532                    break;
533                }
534                sb.append(c[0]);
535            }
536
537            if (separated && !"EOF".equals(separator)) {
538                // we still need to read the separator and account for it under readCount
539                if (r.read(c) < 0) {
540                    throw new EOFException();
541                } else {
542                    readCount++;
543                    // BBB extra check, left commented out for now (we don't want to break existing code
544//                    if (c[0] != separatorChar)
545//                        throw new IOException("Separator '"+separatorChar+"' expected "+
546//                              "but found character '"+c[0]+"' instead.");
547                }
548            }
549        }
550
551        return sb.toString();
552    }
553
554    protected String readField (InputStreamReader r, String fieldName, int len,
555                                String type, String separator) throws IOException
556    {
557        String fieldValue = read (r, len, type, separator);
558
559        if (isBinary(type))
560            fieldValue = ISOUtil.hexString (fieldValue.getBytes (charset));
561        fields.put (fieldName, fieldValue);
562        // System.out.println ("++++ "+fieldName + ":" + fieldValue + " " + type + "," + isBinary(type));
563        return fieldValue;
564    }
565
566    public void set (String name, String value) {
567        if (value != null)
568            fields.put (name, value);
569        else
570            fields.remove (name);
571    }
572    public void setHeader (byte[] h) {
573        this.header = h;
574    }
575    public byte[] getHeader () {
576        return header;
577    }
578    public String getHexHeader () {
579        return header != null ? ISOUtil.hexString (header).substring (2) : "";
580    }
581    public String get (String fieldName) {
582        return fields.get (fieldName);
583    }
584    public String get (String fieldName, String def) {
585        String s = fields.get (fieldName);
586        return s != null ? s : def;
587    }
588    public void copy (String fieldName, FSDMsg msg) {
589        fields.put (fieldName, msg.get (fieldName));
590    }
591    public void copy (String fieldName, FSDMsg msg, String def) {
592        fields.put (fieldName, msg.get(fieldName, def));
593    }
594    public byte[] getHexBytes (String name) {
595        String s = get (name);
596        return s == null ? null : ISOUtil.hex2byte (s);
597    }
598    @SuppressWarnings("PMD.EmptyCatchBlock")
599    public int getInt (String name) {
600        int i = 0;
601        try {
602            i = Integer.parseInt (get (name));
603        } catch (Exception ignored) { }
604        return i;
605    }
606    @SuppressWarnings("PMD.EmptyCatchBlock")
607    public int getInt (String name, int def) {
608        int i = def;
609        try {
610            i = Integer.parseInt (get (name));
611        } catch (Exception ignored) { }
612        return i;
613    }
614    public Element toXML () {
615        Element e = new Element ("message");
616        if (header != null) {
617            e.addContent (
618                new Element ("header")
619                    .setText (getHexHeader ())
620            );
621        }
622        for (String fieldName :fields.keySet()) {
623            Element inner = new Element (fieldName);
624            inner.addContent (ISOUtil.normalize (fields.get (fieldName)));
625            e.addContent (inner);
626        }
627        return e;
628    }
629    protected Element getSchema ()
630        throws JDOMException, IOException {
631        return getSchema (baseSchema);
632    }
633    protected Element getSchema (String message)
634        throws JDOMException, IOException {
635        return getSchema (message, "", null);
636    }
637    protected Element getSchema (String prefix, String suffix, String defSuffix)
638        throws JDOMException, IOException {
639        if (basePath == null)
640            throw new NullPointerException("basePath can not be null");
641        StringBuilder sb = new StringBuilder (basePath);
642        sb.append (prefix);
643        prefix = sb.toString(); // little hack, we'll reuse later with defSuffix
644        sb.append (suffix);
645        sb.append (".xml");
646        String uri = sb.toString ();
647
648        Space sp = SpaceFactory.getSpace();
649        Element schema = (Element) sp.rdp (uri);
650        if (schema == null) {
651            schema = loadSchema(uri, defSuffix == null);
652            if (schema == null && defSuffix != null) {
653                sb = new StringBuilder (prefix);
654                sb.append (defSuffix);
655                sb.append (".xml");
656                schema = loadSchema(sb.toString(), true);
657            }
658            sp.out (uri, schema);
659        }
660        return schema;
661    }
662
663    protected Element loadSchema(String uri, boolean throwex)
664        throws JDOMException, IOException {
665        SAXBuilder builder = new SAXBuilder();
666        if (uri.startsWith("jar:") && uri.length()>4) {
667            InputStream is = schemaResouceInputStream(uri.substring(4));
668            if (is == null && throwex)
669                throw new FileNotFoundException(uri + " not found");
670            else if (is != null)
671                return builder.build(is).getRootElement();
672            else
673                return null;
674        }
675
676        URL url = new URL(uri);
677        try {
678            return builder.build(url).getRootElement();
679        } catch (FileNotFoundException ex) {
680            if (throwex)
681                throw ex;
682            return null;
683        }
684    }
685
686    protected InputStream schemaResouceInputStream(String resource)
687        throws JDOMException, IOException {
688        ClassLoader cl = Thread.currentThread().getContextClassLoader();
689        cl = cl==null ? ClassLoader.getSystemClassLoader() : cl;
690        return cl.getResourceAsStream(resource);
691    }
692
693    /**
694     * @return message's Map
695     */
696    public Map getMap () {
697        return fields;
698    }
699    public void setMap (Map fields) {
700        this.fields = fields;
701    }
702
703    @Override
704    public void dump (PrintStream p, String indent) {
705        String inner = indent + "  ";
706        p.println (indent + "<fsdmsg schema='" + basePath + baseSchema  + "'>");
707        if (header != null) {
708            append (p, "header", getHexHeader(), inner);
709        }
710        for (String f :fields.keySet())
711            append (p, f, fields.get (f), inner);
712        p.println (indent + "</fsdmsg>");
713    }
714    private void append (PrintStream p, String f, String v, String indent) {
715        p.println (indent + f + ": '" + v + "'");
716    }
717    public boolean hasField(String fieldName) {
718        return fields.containsKey(fieldName);
719    }
720
721    @Override
722    public Object clone() {
723        try {
724            FSDMsg m = (FSDMsg) super.clone();
725            m.fields = (Map) ((LinkedHashMap) fields).clone();
726            return m;
727        } catch (CloneNotSupportedException e) {
728            throw new InternalError();
729        }
730    }
731    public void merge (FSDMsg m) {
732        for (Entry<String,String> entry: m.fields.entrySet())
733             set (entry.getKey(), entry.getValue());
734    }
735
736    @Override
737    public boolean equals(Object o) {
738        if (this == o) return true;
739        if (o == null || getClass() != o.getClass()) return false;
740        FSDMsg fsdMsg = (FSDMsg) o;
741        return Objects.equals(fields, fsdMsg.fields) &&
742          Objects.equals(separators, fsdMsg.separators) &&
743          Objects.equals(baseSchema, fsdMsg.baseSchema) &&
744          Objects.equals(basePath, fsdMsg.basePath) &&
745          Arrays.equals(header, fsdMsg.header) &&
746          Objects.equals(charset, fsdMsg.charset);
747    }
748
749    @Override
750    public int hashCode() {
751        int result = Objects.hash(fields, separators, baseSchema, basePath, charset);
752        result = 31 * result + Arrays.hashCode(header);
753        return result;
754    }
755}