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 * <h2>How to use</h2> 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 * Supported field separators are: 081 * <dl> 082 * <dt>FS</dt><dd>Field separator using '034' as the separator.</dd> 083 * <dt>US</dt><dd>Field separator using '037' as the separator.</dd> 084 * <dt>GS</dt><dd>Group separator using '035' as the separator.</dd> 085 * <dt>RS</dt><dd>Row separator using '036' as the separator.</dd> 086 * <dt>PIPE</dt><dd>Field separator using '|' as the separator.</dd> 087 * <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 088 * parsing a message, then no exception is thrown.</dd> 089 * <dt>DS</dt><dd>A dummy separator. This is similar to EOF, but the message stream must not end before it is allowed.</dd> 090 * <dt>EOM</dt><dd>End of message separator. This reads all bytes available in the stream. 091 * </dl> 092 * <p> 093 * Key fields allow you to specify a tree of possible message formats. The key fields are the fork points of the tree. 094 * Multiple key fields are supported. It is also possible to have more key fields specified in appended schemas. 095 * </p> 096 * 097 * @author Alejandro Revila 098 * @author Mark Salter 099 * @author Dave Bergert 100 * @since 1.4.7 101 */ 102@SuppressWarnings("unchecked") 103public class FSDMsg implements Loggeable, Cloneable { 104 /** Field Separator character (ASCII 0x1C). */ 105 public static char FS = '\034'; 106 /** Unit Separator character (ASCII 0x1F). */ 107 public static char US = '\037'; 108 /** Group Separator character (ASCII 0x1D). */ 109 public static char GS = '\035'; 110 /** Record Separator character (ASCII 0x1E). */ 111 public static char RS = '\036'; 112 /** End of File character (null byte). */ 113 public static char EOF = '\000'; 114 /** Pipe character (ASCII 0x7C). */ 115 public static char PIPE = '\u007C'; 116 /** End of Message marker (null byte). */ 117 public static char EOM = '\000'; 118 119 private static final Set<String> DUMMY_SEPARATORS = new HashSet<>(Arrays.asList("DS", "EOM")); 120 private static final String EOM_SEPARATOR = "EOM"; 121 private static final int READ_BUFFER = 8192; 122 123 Map<String,String> fields; 124 Map<String, Character> separators; 125 126 String baseSchema; 127 String basePath; 128 byte[] header; 129 Charset charset; 130 private int readCount; 131 132 /** 133 * Creates a FSDMsg with a specific base path for the message format schema. 134 * @param basePath schema path, for example: "file:src/data/NDC-" looks for a file src/data/NDC-base.xml 135 */ 136 public FSDMsg (String basePath) { 137 this (basePath, "base"); 138 } 139 140 /** 141 * Creates a FSDMsg with a specific base path for the message format schema, and a base schema name. For instance, 142 * FSDMsg("file:src/data/NDC-", "root") will look for a file: src/data/NDC-root.xml 143 * @param basePath schema path 144 * @param baseSchema schema name 145 */ 146 public FSDMsg (String basePath, String baseSchema) { 147 super(); 148 fields = new LinkedHashMap<>(); 149 separators = new LinkedHashMap<>(); 150 this.basePath = basePath; 151 this.baseSchema = baseSchema; 152 charset = ISOUtil.CHARSET; 153 readCount = 0; 154 155 setSeparator("FS", FS); 156 setSeparator("US", US); 157 setSeparator("GS", GS); 158 setSeparator("RS", RS); 159 setSeparator("EOF", EOF); 160 setSeparator("PIPE", PIPE); 161 } 162 /** 163 * Returns the base schema path used to load field definitions. 164 * @return the base schema path 165 */ 166 public String getBasePath() { 167 return basePath; 168 } 169 /** 170 * Returns the base schema name used to load field definitions. 171 * @return the base schema name 172 */ 173 public String getBaseSchema() { 174 return baseSchema; 175 } 176 177 /** 178 * Sets the character set used for packing and unpacking string fields. 179 * @param charset the character set to use 180 */ 181 public void setCharset(Charset charset) { 182 this.charset = charset; 183 } 184 185 /** 186 * Adds or overrides a separator type/char pair. 187 * @param separatorName string identifier for the separator type (e.g. "FS", "US") 188 * @param separator the character representing this separator 189 */ 190 public void setSeparator(String separatorName, char separator) { 191 separators.put(separatorName, separator); 192 } 193 194 /** 195 * Removes a previously defined separator type. 196 * @param separatorName string identifier for the separator type to remove 197 * @throws IllegalArgumentException if the separator was not previously defined 198 */ 199 public void unsetSeparator(String separatorName) { 200 if (!separators.containsKey(separatorName)) 201 throw new IllegalArgumentException("unsetSeparator was attempted for "+ 202 separatorName+" which was not previously defined."); 203 204 separators.remove(separatorName); 205 } 206 207 /** 208 * parse message. If the stream ends before the message is completely read, then the method adds an EOF field. 209 * 210 * @param is input stream 211 * 212 * @throws IOException on I/O error 213 * @throws JDOMException on XML parsing error 214 */ 215 public void unpack (InputStream is) 216 throws IOException, JDOMException { 217 try { 218 if (is.markSupported()) 219 is.mark(READ_BUFFER); 220 unpack (new InputStreamReader(is, charset), getSchema (baseSchema)); 221 if (is.markSupported()) { 222 is.reset(); 223 is.skip (readCount); 224 readCount = 0; 225 } 226 } catch (EOFException e) { 227 if (!fields.isEmpty()) 228 fields.put ("EOF", "true"); // some fields were read, but unexpected EOF found 229 else // nothing new since last msg, fields were read; no more msgs from this stream 230 throw e; // just rethrow the exception 231 } 232 } 233 /** 234 * parse message. If the stream ends before the message is completely read, then the method adds an EOF field. 235 * 236 * @param b message image 237 * 238 * @throws IOException on I/O error while reading the byte array 239 * @throws JDOMException on schema parsing error 240 */ 241 public void unpack (byte[] b) 242 throws IOException, JDOMException { 243 unpack (new ByteArrayInputStream (b)); 244 } 245 246 /** 247 * Packs this FSDMsg into its string representation. 248 * @return the packed message string 249 * @throws org.jdom2.JDOMException on schema parsing error 250 * @throws java.io.IOException on I/O error 251 * @throws ISOException on packing error 252 */ 253 public String pack () 254 throws JDOMException, IOException, ISOException 255 { 256 StringBuilder sb = new StringBuilder (); 257 pack (getSchema (baseSchema), sb); 258 return sb.toString (); 259 } 260 /** 261 * Packs this message into a byte array. 262 * @return the packed bytes 263 * @throws ISOException on pack error 264 * @throws IOException on I/O error 265 * @throws JDOMException on schema parse error 266 */ 267 public byte[] packToBytes () 268 throws JDOMException, IOException, ISOException 269 { 270 return pack().getBytes(charset); 271 } 272 273 /** 274 * Returns the formatted value for the named field. 275 * @param id field identifier 276 * @param type field type 277 * @param length field length 278 * @param defValue default value if field is absent 279 * @param separator field separator 280 * @return formatted field value 281 * @throws ISOException on error 282 */ 283 protected String get (String id, String type, int length, String defValue, String separator) 284 throws ISOException 285 { 286 return get(id,type,length,defValue,separator,true); 287 } 288 /** 289 * Returns the formatted value for the named field with optional unpadding. 290 * @param id field identifier 291 * @param type field type 292 * @param length field length 293 * @param defValue default value 294 * @param separator field separator 295 * @param unPad if true, strip padding 296 * @return formatted field value 297 * @throws ISOException on error 298 */ 299 protected String get (String id, String type, int length, String defValue, String separator, boolean unPad) 300 throws ISOException 301 { 302 String value = fields.get (id); 303 if (value == null) 304 value = defValue == null ? "" : defValue; 305 306 type = type.toUpperCase (); 307 int lengthLength = 0; 308 while (type.charAt(0) == 'L') { 309 lengthLength++; 310 type = type.substring(1); 311 } 312 313 switch (type.charAt (0)) { 314 case 'N': 315 if (!isSeparated(separator)) { 316 value = ISOUtil.zeropad (value, length); 317 } // else Leave value unpadded. 318 break; 319 case 'A': 320 if (!isSeparated(separator) && lengthLength == 0) { 321 value = ISOUtil.strpad (value, length); 322 } // else Leave value unpadded. 323 if (value.length() > length) 324 value = value.substring(0,length); 325 break; 326 case 'K': 327 if (defValue != null) 328 value = defValue; 329 break; 330 case 'B': 331 if (length << 1 < value.length()) 332 throw new IllegalArgumentException("field content=" + value 333 + " is too long to fit in field " + id 334 + " whose length is " + length); 335 336 if (isSeparated(separator)) { 337 // Convert but do not pad if this field ends with a 338 // separator 339 value = new String(ISOUtil.hex2byte(value), charset); 340 } else { 341 value = new String(ISOUtil.hex2byte(ISOUtil.zeropad( 342 value, length << 1).substring(0, length << 1)), charset); 343 } 344 break; 345 } 346 347 if (lengthLength == 0 && (!isSeparated(separator) || isBinary(type) || EOM_SEPARATOR.equals(separator) ||(isSeparated(separator) && !unPad))) 348 return value; 349 else { 350 if (lengthLength > 0) { 351 String format = String.format("%%0%dd%%s", lengthLength); 352 value = String.format(format, value.length(), value); 353 } else { 354 value = ISOUtil.blankUnPad(value); 355 } 356 } 357 return value; 358 } 359 360 private boolean isSeparated(String separator) { 361 /* 362 * if type's last two characters appear in our Map of separators, 363 * return true 364 */ 365 if (separator == null) 366 return false; 367 else if (separators.containsKey (separator)) 368 return true; 369 else if (isDummySeparator (separator)) 370 return true; 371 else 372 try { 373 if (Character.isDefined(Integer.parseInt(separator,16))) { 374 setSeparator(separator, (char)Long.parseLong(separator,16)); 375 return true; 376 } 377 } catch (NumberFormatException ignored) { 378 throw new IllegalArgumentException("Invalid separator '"+ separator + "'"); 379 } 380 throw new IllegalArgumentException("isSeparated called on separator="+ 381 separator+" which was not previously defined."); 382 } 383 384 private boolean isDummySeparator(String separator) { 385 return DUMMY_SEPARATORS.contains(separator); 386 } 387 388 private boolean isBinary(String type) { 389 /* 390 * if type's first digit is a 'B' return true 391 */ 392 return type.startsWith("B"); 393 } 394 395 /** 396 * Tests whether a byte matches any configured separator character. 397 * @param b the byte to test 398 * @return true if the byte corresponds to any configured separator 399 */ 400 public boolean isSeparator(byte b) { 401 return separators.containsValue((char) b); 402 } 403 404 private String getSeparatorType(String type) { 405 if (type.length() > 2 && !(type.charAt(0) == 'L')) { 406 return type.substring(1); 407 } 408 return null; 409 } 410 411 private char getSeparator(String separator) { 412 if (separators.containsKey(separator)) 413 return separators.get(separator); 414 else if (isDummySeparator (separator)) { 415 // Dummy separator type, return 0 to indicate nothing to add. 416 return 0; 417 } 418 419 throw new IllegalArgumentException("getSeparator called on separator="+ 420 separator+" which was not previously defined."); 421 } 422 423 /** 424 * Packs this message into the given StringBuilder using the provided schema. 425 * @param schema the schema element 426 * @param sb the target StringBuilder 427 * @throws ISOException on pack error 428 * @throws IOException on I/O error 429 * @throws JDOMException on schema error 430 */ 431 protected void pack (Element schema, StringBuilder sb) 432 throws JDOMException, IOException, ISOException 433 { 434 String keyOff = ""; 435 String defaultKey = ""; 436 for (Element elem : schema.getChildren("field")) { 437 String id = elem.getAttributeValue ("id"); 438 int length = Integer.parseInt (elem.getAttributeValue ("length")); 439 String type = elem.getAttributeValue ("type"); 440 // For backward compatibility, look for a separator at the end of the type attribute, if no separator has been defined. 441 String separator = elem.getAttributeValue ("separator"); 442 if (type != null && separator == null) { 443 separator = getSeparatorType (type); 444 } 445 boolean unPad = true; 446 if (separator != null && elem.getAttributeValue("pack_unpad") != null) { 447 unPad = Boolean.valueOf(elem.getAttributeValue("pack_unpad")); 448 } 449 boolean key = "true".equals (elem.getAttributeValue ("key")); 450 Map properties = key ? loadProperties(elem) : Collections.EMPTY_MAP; 451 String defValue = elem.getText(); 452 // 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. 453 if (!properties.isEmpty()) { 454 defValue = defValue.replace("\n", "").replace("\t", "").replace("\r", ""); 455 } 456 String value = get (id, type, length, defValue, separator, unPad); 457 sb.append (value); 458 459 if (isSeparated(separator)) { 460 char c = getSeparator(separator); 461 if (c > 0) 462 sb.append(c); 463 } 464 if (key) { 465 String v = isBinary(type) ? ISOUtil.hexString(value.getBytes(charset)) : value; 466 keyOff = keyOff + normalizeKeyValue(v, properties); 467 defaultKey += elem.getAttributeValue ("default-key"); 468 } 469 } 470 if (keyOff.length() > 0) 471 pack (getSchema (getId (schema), keyOff, defaultKey), sb); 472 } 473 474 private Map loadProperties(Element elem) { 475 Map props = new HashMap (); 476 for (Element prop : elem.getChildren ("property")) { 477 String name = prop.getAttributeValue ("name"); 478 String value = prop.getAttributeValue ("value"); 479 props.put (name, value); 480 } 481 return props; 482 } 483 484 private String normalizeKeyValue(String value, Map<?,String> properties) { 485 if (properties.containsKey(value)) { 486 return properties.get(value); 487 } 488 return ISOUtil.normalize(value); 489 } 490 491 /** 492 * Unpacks a message from the reader using the provided schema. 493 * @param r the reader 494 * @param schema the schema element 495 * @throws IOException on I/O error 496 * @throws JDOMException on schema error 497 */ 498 protected void unpack (InputStreamReader r, Element schema) 499 throws IOException, JDOMException { 500 501 String keyOff = ""; 502 String defaultKey = ""; 503 for (Element elem : schema.getChildren("field")) { 504 String id = elem.getAttributeValue ("id"); 505 int length = Integer.parseInt (elem.getAttributeValue ("length")); 506 String type = elem.getAttributeValue ("type").toUpperCase(); 507 String separator = elem.getAttributeValue ("separator"); 508 if (/* type != null && */ // can't be null or we would have NPE'ed when .toUpperCase() 509 separator == null) { 510 separator = getSeparatorType (type); 511 } 512 boolean key = "true".equals (elem.getAttributeValue ("key")); 513 Map properties = key ? loadProperties(elem) : Collections.EMPTY_MAP; 514 515 String value = readField(r, id, length, type, separator); 516 517 if (key) { 518 keyOff = keyOff + normalizeKeyValue(value, properties); 519 defaultKey += elem.getAttributeValue ("default-key"); 520 } 521 522 // constant fields should have read the constant value 523 if ("K".equals(type) && !value.equals (elem.getText())) 524 throw new IllegalArgumentException ( 525 "Field "+id 526 + " value='" +value 527 + "' expected='" + elem.getText () + "'" 528 ); 529 } 530 if (keyOff.length() > 0) { 531 unpack(r, getSchema (getId (schema), keyOff, defaultKey)); // recursion 532 } 533 } 534 private String getId (Element e) { 535 String s = e.getAttributeValue ("id"); 536 return s == null ? "" : s; 537 } 538 /** 539 * Reads a field value from the stream. 540 * @param r the reader 541 * @param len maximum field length 542 * @param type field type 543 * @param separator field separator 544 * @return the field value 545 * @throws IOException on I/O error 546 */ 547 protected String read (InputStreamReader r, int len, String type, String separator) 548 throws IOException 549 { 550 StringBuilder sb = new StringBuilder(); 551 char[] c = new char[1]; 552 boolean expectSeparator = isSeparated(separator); 553 boolean separated = expectSeparator; 554 char separatorChar= expectSeparator ? getSeparator(separator) : '\0'; 555 556 if (EOM_SEPARATOR.equals(separator)) { 557 // Grab what's left. 558 char[] rest = new char[32]; 559 int con; 560 while ((con = r.read(rest, 0, rest.length)) >= 0) { 561 readCount += con; 562 if (rest.length == con) 563 sb.append(rest); 564 else 565 sb.append(Arrays.copyOf(rest, con)); 566 } 567 } else if (isDummySeparator(separator)) { 568 /* 569 * No need to look for a separator, that is not there! Try and take 570 * len bytes from the stream. 571 */ 572 for (int i = 0; i < len; i++) { 573 if (r.read(c) < 0) { 574 break; // end of stream indicates end of field? 575 } 576 readCount++; 577 sb.append(c[0]); 578 } 579 } else { 580 int lengthLength = 0; 581 if (type != null && type.startsWith("L")) { 582 while (type.charAt(0) == 'L') { 583 lengthLength++; 584 type = type.substring(1); 585 } 586 if (lengthLength > 0) { 587 char[] ll = new char[lengthLength]; 588 if (r.read(ll) != lengthLength) 589 throw new EOFException(); 590 len = Integer.parseInt(new String(ll)); 591 } 592 } 593 for (int i = 0; i < len; i++) { 594 if (r.read(c) < 0) { 595 if (!"EOF".equals(separator)) 596 throw new EOFException(); 597 else { 598 separated = false; 599 break; 600 } 601 } 602 readCount++; 603 if (expectSeparator && c[0] == separatorChar) { 604 separated = false; 605 break; 606 } 607 sb.append(c[0]); 608 } 609 610 if (separated && !"EOF".equals(separator)) { 611 // we still need to read the separator and account for it under readCount 612 if (r.read(c) < 0) { 613 throw new EOFException(); 614 } else { 615 readCount++; 616 // BBB extra check, left commented out for now (we don't want to break existing code 617// if (c[0] != separatorChar) 618// throw new IOException("Separator '"+separatorChar+"' expected "+ 619// "but found character '"+c[0]+"' instead."); 620 } 621 } 622 } 623 624 return sb.toString(); 625 } 626 627 /** 628 * Reads a named field value from the stream. 629 * @param r the reader 630 * @param fieldName the field name 631 * @param len maximum field length 632 * @param type field type 633 * @param separator field separator 634 * @return the field value 635 * @throws IOException on I/O error 636 */ 637 protected String readField (InputStreamReader r, String fieldName, int len, 638 String type, String separator) throws IOException 639 { 640 String fieldValue = read (r, len, type, separator); 641 642 if (isBinary(type)) 643 fieldValue = ISOUtil.hexString (fieldValue.getBytes (charset)); 644 fields.put (fieldName, fieldValue); 645 // System.out.println ("++++ "+fieldName + ":" + fieldValue + " " + type + "," + isBinary(type)); 646 return fieldValue; 647 } 648 649 /** 650 * Sets a field value; removes the field if value is null. 651 * @param name the field name 652 * @param value the field value, or null to remove 653 */ 654 public void set (String name, String value) { 655 if (value != null) 656 fields.put (name, value); 657 else 658 fields.remove (name); 659 } 660 /** 661 * Sets the binary header bytes for this message. 662 * @param h the header bytes to set 663 */ 664 public void setHeader (byte[] h) { 665 this.header = h; 666 } 667 /** 668 * Returns the binary header bytes. 669 * @return the header bytes, or null if not set 670 */ 671 public byte[] getHeader () { 672 return header; 673 } 674 /** 675 * Returns the header as a hex string, or empty string if no header is set. 676 * @return the header as a hex string 677 */ 678 public String getHexHeader () { 679 return header != null ? ISOUtil.hexString (header).substring (2) : ""; 680 } 681 /** 682 * Returns the value of the named field. 683 * @param fieldName the field name to retrieve 684 * @return the field value, or null if not set 685 */ 686 public String get (String fieldName) { 687 return fields.get (fieldName); 688 } 689 /** 690 * Returns the value of the named field, or a default if not set. 691 * @param fieldName the field name 692 * @param def the default value if not set 693 * @return the field value, or the default 694 */ 695 public String get (String fieldName, String def) { 696 String s = fields.get (fieldName); 697 return s != null ? s : def; 698 } 699 /** 700 * Copies a field value from another FSDMsg into this message. 701 * @param fieldName the field to copy 702 * @param msg the source FSDMsg 703 */ 704 public void copy (String fieldName, FSDMsg msg) { 705 fields.put (fieldName, msg.get (fieldName)); 706 } 707 /** 708 * Copies a field value from another FSDMsg into this message, using a default if not present. 709 * @param fieldName the field to copy 710 * @param msg the source FSDMsg 711 * @param def default value if the field is not present in msg 712 */ 713 public void copy (String fieldName, FSDMsg msg, String def) { 714 fields.put (fieldName, msg.get(fieldName, def)); 715 } 716 /** 717 * Returns the value of the named field decoded from hex. 718 * @param name the field name containing a hex-encoded value 719 * @return decoded bytes, or null if field not set 720 */ 721 public byte[] getHexBytes (String name) { 722 String s = get (name); 723 return s == null ? null : ISOUtil.hex2byte (s); 724 } 725 /** 726 * Returns the integer value of the named field, or 0 if absent or non-numeric. 727 * @param name the field name 728 * @return integer value of the field 729 */ 730 @SuppressWarnings("PMD.EmptyCatchBlock") 731 public int getInt (String name) { 732 int i = 0; 733 try { 734 i = Integer.parseInt (get (name)); 735 } catch (Exception ignored) { } 736 return i; 737 } 738 /** 739 * Returns the integer value of the named field, or a default if absent or non-numeric. 740 * @param name the field name 741 * @param def the default value to return if the field is absent or non-numeric 742 * @return integer value of the field or the default 743 */ 744 @SuppressWarnings("PMD.EmptyCatchBlock") 745 public int getInt (String name, int def) { 746 int i = def; 747 try { 748 i = Integer.parseInt (get (name)); 749 } catch (Exception ignored) { } 750 return i; 751 } 752 /** 753 * Serializes this FSDMsg to a JDOM XML Element. 754 * @return XML Element representing this message 755 */ 756 public Element toXML () { 757 Element e = new Element ("message"); 758 if (header != null) { 759 e.addContent ( 760 new Element ("header") 761 .setText (getHexHeader ()) 762 ); 763 } 764 for (String fieldName :fields.keySet()) { 765 Element inner = new Element (fieldName); 766 inner.addContent (ISOUtil.normalize (fields.get (fieldName))); 767 e.addContent (inner); 768 } 769 return e; 770 } 771 /** 772 * Returns the root schema Element for the base schema. 773 * @return the root schema Element 774 * @throws JDOMException on XML parsing error 775 * @throws IOException on I/O error 776 */ 777 protected Element getSchema () 778 throws JDOMException, IOException { 779 return getSchema (baseSchema); 780 } 781 /** 782 * Returns the root schema Element for the named message schema. 783 * @param message the schema message name 784 * @return the root schema Element 785 * @throws JDOMException on XML parsing error 786 * @throws IOException on I/O error 787 */ 788 protected Element getSchema (String message) 789 throws JDOMException, IOException { 790 return getSchema (message, "", null); 791 } 792 /** 793 * Returns the root schema Element located at the given path. 794 * @param prefix the schema path prefix 795 * @param suffix the schema path suffix 796 * @param defSuffix the default suffix to use if the path is not found 797 * @return the root schema Element 798 * @throws JDOMException on XML parsing error 799 * @throws IOException on I/O error 800 */ 801 protected Element getSchema (String prefix, String suffix, String defSuffix) 802 throws JDOMException, IOException { 803 if (basePath == null) 804 throw new NullPointerException("basePath can not be null"); 805 StringBuilder sb = new StringBuilder (basePath); 806 sb.append (prefix); 807 prefix = sb.toString(); // little hack, we'll reuse later with defSuffix 808 sb.append (suffix); 809 sb.append (".xml"); 810 String uri = sb.toString (); 811 812 Space sp = SpaceFactory.getSpace(); 813 Element schema = (Element) sp.rdp (uri); 814 if (schema == null) { 815 schema = loadSchema(uri, defSuffix == null); 816 if (schema == null && defSuffix != null) { 817 sb = new StringBuilder (prefix); 818 sb.append (defSuffix); 819 sb.append (".xml"); 820 schema = loadSchema(sb.toString(), true); 821 } 822 sp.out (uri, schema); 823 } 824 return schema; 825 } 826 827 /** 828 * Loads and parses the schema from the given URI. 829 * @param uri the URI of the schema resource to load 830 * @param throwex if true, throw an exception if the resource is not found 831 * @return the parsed root Element, or null if not found and throwex is false 832 * @throws JDOMException on XML parsing error 833 * @throws IOException on I/O error 834 */ 835 protected Element loadSchema(String uri, boolean throwex) 836 throws JDOMException, IOException { 837 SAXBuilder builder = new SAXBuilder(); 838 if (uri.startsWith("jar:") && uri.length()>4) { 839 InputStream is = schemaResouceInputStream(uri.substring(4)); 840 if (is == null && throwex) 841 throw new FileNotFoundException(uri + " not found"); 842 else if (is != null) 843 return builder.build(is).getRootElement(); 844 else 845 return null; 846 } 847 848 URL url = new URL(uri); 849 try { 850 return builder.build(url).getRootElement(); 851 } catch (FileNotFoundException ex) { 852 if (throwex) 853 throw ex; 854 return null; 855 } 856 } 857 858 /** 859 * Returns an InputStream for the given classpath resource. 860 * @param resource the classpath resource path 861 * @return the InputStream, or null if the resource is not found 862 * @throws IOException on I/O error 863 * @throws JDOMException on XML error 864 */ 865 protected InputStream schemaResouceInputStream(String resource) 866 throws JDOMException, IOException { 867 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 868 cl = cl==null ? ClassLoader.getSystemClassLoader() : cl; 869 return cl.getResourceAsStream(resource); 870 } 871 872 /** 873 * @return message's Map 874 */ 875 /** 876 * Returns the underlying fields map. 877 * @return the fields map 878 */ 879 public Map getMap () { 880 return fields; 881 } 882 /** 883 * Sets the underlying fields map. 884 * @param fields the fields map to set 885 */ 886 public void setMap (Map fields) { 887 this.fields = fields; 888 } 889 890 @Override 891 public void dump (PrintStream p, String indent) { 892 String inner = indent + " "; 893 p.println (indent + "<fsdmsg schema='" + basePath + baseSchema + "'>"); 894 if (header != null) { 895 append (p, "header", getHexHeader(), inner); 896 } 897 for (String f :fields.keySet()) 898 append (p, f, fields.get (f), inner); 899 p.println (indent + "</fsdmsg>"); 900 } 901 private void append (PrintStream p, String f, String v, String indent) { 902 p.println (indent + f + ": '" + v + "'"); 903 } 904 /** 905 * Returns true if this message has a value for the named field. 906 * @param fieldName the field name to check 907 * @return true if the field is present 908 */ 909 public boolean hasField(String fieldName) { 910 return fields.containsKey(fieldName); 911 } 912 913 @Override 914 public Object clone() { 915 try { 916 FSDMsg m = (FSDMsg) super.clone(); 917 m.fields = (Map) ((LinkedHashMap) fields).clone(); 918 return m; 919 } catch (CloneNotSupportedException e) { 920 throw new InternalError(); 921 } 922 } 923 /** 924 * Copies all fields from the given message into this message. 925 * @param m the source message to merge from 926 */ 927 public void merge (FSDMsg m) { 928 for (Entry<String,String> entry: m.fields.entrySet()) 929 set (entry.getKey(), entry.getValue()); 930 } 931 932 @Override 933 public boolean equals(Object o) { 934 if (this == o) return true; 935 if (o == null || getClass() != o.getClass()) return false; 936 FSDMsg fsdMsg = (FSDMsg) o; 937 return Objects.equals(fields, fsdMsg.fields) && 938 Objects.equals(separators, fsdMsg.separators) && 939 Objects.equals(baseSchema, fsdMsg.baseSchema) && 940 Objects.equals(basePath, fsdMsg.basePath) && 941 Arrays.equals(header, fsdMsg.header) && 942 Objects.equals(charset, fsdMsg.charset); 943 } 944 945 @Override 946 public int hashCode() { 947 int result = Objects.hash(fields, separators, baseSchema, basePath, charset); 948 result = 31 * result + Arrays.hashCode(header); 949 return result; 950 } 951}