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}