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.packager; 020 021import org.jpos.iso.*; 022import org.jpos.iso.header.BaseHeader; 023import org.jpos.util.LogEvent; 024import org.jpos.util.LogSource; 025import org.jpos.util.Logger; 026import org.xml.sax.Attributes; 027import org.xml.sax.InputSource; 028import org.xml.sax.SAXException; 029import org.xml.sax.XMLReader; 030import org.xml.sax.helpers.DefaultHandler; 031import org.xml.sax.helpers.XMLReaderFactory; 032 033import java.io.*; 034import java.math.BigDecimal; 035import java.nio.charset.StandardCharsets; 036import java.util.Stack; 037import java.util.concurrent.locks.Lock; 038import java.util.concurrent.locks.ReentrantLock; 039 040/** 041 * packs/unpacks ISOMsgs into XML representation 042 * 043 * @author apr@cs.com.uy 044 * @version $Id$ 045 * @see ISOPackager 046 */ 047@SuppressWarnings("unchecked") 048public class XMLPackager extends DefaultHandler 049 implements ISOPackager, LogSource 050{ 051 /** Logger used to emit pack/unpack events. */ 052 protected Logger logger = null; 053 /** Logging realm associated with this packager. */ 054 protected String realm = null; 055 private XMLReader reader; 056 private Stack stk; 057 private Lock parserLock = new ReentrantLock(); 058 059 /** XML element name used for ISO messages. */ 060 public static final String ISOMSG_TAG = "isomsg"; 061 /** XML element name used for ISO fields. */ 062 public static final String ISOFIELD_TAG = "field"; 063 /** XML attribute name holding field identifiers. */ 064 public static final String ID_ATTR = "id"; 065 /** XML attribute name holding scalar values. */ 066 public static final String VALUE_ATTR = "value"; 067 /** XML attribute name holding type metadata. */ 068 public static final String TYPE_ATTR = "type"; 069 /** XML type marker for binary field content. */ 070 public static final String TYPE_BINARY = "binary"; 071 /** XML type marker for bitmap field content. */ 072 public static final String TYPE_BITMAP = "bitmap"; 073 /** XML type marker for amount field content. */ 074 public static final String TYPE_AMOUNT = "amount"; 075 /** XML type marker for dataset field content. */ 076 public static final String TYPE_DATASET = "dataset"; 077 /** XML attribute name holding currency metadata. */ 078 public static final String CURRENCY_ATTR = "currency"; 079 /** XML element name used for message headers. */ 080 public static final String HEADER_TAG = "header"; 081 /** XML attribute name used for charset declarations. */ 082 public static final String ENCODING_ATTR = "encoding"; 083 /** Literal name for ASCII encoding declarations. */ 084 public static final String ASCII_ENCODING= "ascii"; 085 /** XML element name used for datasets. */ 086 public static final String DATASET_TAG = "dataset"; 087 /** XML element name used for dataset elements. */ 088 public static final String ELEMENT_TAG = "element"; 089 /** XML attribute name holding dataset format metadata. */ 090 public static final String FORMAT_ATTR = "format"; 091 092 // fields that will be forced to be interpreted as binary data 093 private int[] binaryFields= null; 094 095 /** 096 * Creates an XML packager with a hardened SAX parser configuration. 097 * 098 * @throws ISOException if the XML reader cannot be created or configured 099 */ 100 public XMLPackager() throws ISOException { 101 super(); 102 stk = new Stack(); 103 try { 104 reader = createXMLReader(); 105 106 // some parser restrictions have been set for security and maybe PCI compliance 107 setXMLParserFeature("http://xml.org/sax/features/validation", false); 108 setXMLParserFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 109 setXMLParserFeature("http://xml.org/sax/features/external-general-entities", false); 110 setXMLParserFeature("http://xml.org/sax/features/external-parameter-entities", false); 111 } catch (Exception e) { 112 throw new ISOException (e.toString()); 113 } 114 } 115 116 /** 117 * Marks the supplied fields so their XML values are always decoded as binary. 118 * 119 * @param bfields field numbers to force as binary 120 */ 121 public void forceBinary(int ... bfields) { 122 binaryFields= bfields; 123 } 124 125 public byte[] pack (ISOComponent c) throws ISOException { 126 LogEvent evt = new LogEvent (this, "pack"); 127 128 try { 129 if (!(c instanceof ISOMsg m)) 130 throw new ISOException ("cannot pack "+c.getClass()); 131 132 // heuristics for initial size 40: a typical <field> with 2 chars of id + 12 of value + 2 indent 133 ByteArrayOutputStream out = new ByteArrayOutputStream(40 * (m.getChildren().size()+2)); 134 PrintStream p = new PrintStream(out, false, StandardCharsets.UTF_8); 135 136 m.setDirection(0); // avoid "direction=xxxxxx" in XML msg 137 m.dump (p, ""); 138 byte[] b = out.toByteArray(); 139 140 if (logger != null) 141 evt.addMessage (m); 142 return b; 143 } catch (ISOException e) { 144 evt.addMessage (e); 145 throw e; 146 } finally { 147 Logger.log(evt); 148 } 149 } 150 151 public int unpack (ISOComponent c, byte[] b) throws ISOException { 152 unpack(c, new InputSource(new ByteArrayInputStream(b))); 153 return b.length; 154 } 155 156 public void unpack (ISOComponent c, InputStream in) throws ISOException, IOException { 157 unpack(c, new InputSource(in)); 158 } 159 160 private void unpack (ISOComponent c, InputSource in) throws ISOException { 161 LogEvent evt = new LogEvent (this, "unpack"); 162 163 parserLock.lock(); 164 try { 165 if (!(c instanceof ISOMsg m)) 166 throw new ISOException("Can't call packager on non Composite"); 167 168 while (!stk.empty()) // purge from possible previous error 169 stk.pop(); 170 171 reader.parse (in); 172 if (stk.empty()) 173 throw new ISOException ("error parsing"); 174 175 ISOMsg m1 = (ISOMsg) stk.pop(); 176 m.merge (m1); 177 m.setHeader (m1.getHeader()); 178 179 fixupBinary(m, binaryFields); 180 181 if (logger != null) 182 evt.addMessage (m); 183 } catch (ISOException e) { 184 evt.addMessage (e); 185 throw e; 186 } catch (IOException e) { 187 evt.addMessage (e); 188 throw new ISOException (e.toString()); 189 } catch (SAXException e) { 190 evt.addMessage (e); 191 throw new ISOException (e.toString()); 192 } finally { 193 Logger.log (evt); 194 parserLock.unlock(); 195 } 196 } 197 198 public void startElement 199 (String ns, String name, String qName, Attributes atts) 200 throws SAXException 201 { 202 try { 203 if (name.equals (ISOMSG_TAG)) { 204 int fieldNumber = parseDecimalId(atts.getValue(ID_ATTR)); 205 if (fieldNumber >= 0) { 206 if (stk.empty()) 207 throw new SAXException ("inner without outer"); 208 209 ISOMsg inner = new ISOMsg(fieldNumber); 210 ((ISOMsg)stk.peek()).set (inner); 211 stk.push (inner); 212 } else { 213 stk.push (new ISOMsg(0)); 214 } 215 } else if (name.equals (ISOFIELD_TAG)) { 216 int fieldNumber = parseDecimalId(atts.getValue(ID_ATTR)); 217 ISOMsg m = (ISOMsg) stk.peek(); 218 String id = atts.getValue(ID_ATTR); 219 String value = atts.getValue(VALUE_ATTR); 220 String type = atts.getValue(TYPE_ATTR); 221 if (id == null) 222 throw new SAXException ("invalid field"); 223 value = value == null ? "" : value; 224 225 ISOComponent ic; 226 if (TYPE_DATASET.equals(type)) { 227 ic = new ISODatasetField(fieldNumber); 228 } 229 else if (TYPE_BINARY.equals (type)) { 230 ic = new ISOBinaryField ( 231 fieldNumber, 232 ISOUtil.hex2byte ( 233 value.getBytes(), 0, value.length()/2 234 ) 235 ); 236 } 237 else if (TYPE_AMOUNT.equals (type)) { 238 ic = new ISOAmount( 239 fieldNumber, 240 Integer.parseInt (atts.getValue(CURRENCY_ATTR)), 241 new BigDecimal (value) 242 ); 243 } 244 else { 245 ic = new ISOField (fieldNumber, ISOUtil.stripUnicode(value)); 246 } 247 m.set (ic); 248 stk.push (ic); 249 } else if (DATASET_TAG.equals(name)) { 250 if (!(stk.peek() instanceof ISODatasetField)) 251 throw new SAXException("dataset without dataset field"); 252 String id = atts.getValue(ID_ATTR); 253 String format = atts.getValue(FORMAT_ATTR); 254 if (id == null || format == null) 255 throw new SAXException("invalid dataset"); 256 ISODataset dataset = new ISODataset(parseHexId(id), DatasetFormat.valueOf(format)); 257 ((ISODatasetField) stk.peek()).addDataset(dataset); 258 stk.push(dataset); 259 } else if (ELEMENT_TAG.equals(name)) { 260 if (!(stk.peek() instanceof ISODataset)) 261 throw new SAXException("element without dataset"); 262 ISODataset dataset = (ISODataset) stk.peek(); 263 String id = atts.getValue(ID_ATTR); 264 String value = atts.getValue(VALUE_ATTR); 265 if (id == null) 266 throw new SAXException("invalid dataset element"); 267 int elementId = dataset.getFormat() == DatasetFormat.TLV ? parseHexId(id) : parseDecimalOrHexId(id); 268 byte[] bytes = value == null ? new byte[0] : ISOUtil.hex2byte(value.getBytes(), 0, value.length() / 2); 269 dataset.addElement(elementId, new ISOBinaryField(elementId, bytes), dataset.getFormat() == DatasetFormat.TLV && isConstructedTag(elementId)); 270 } else if (HEADER_TAG.equals (name)) { 271 BaseHeader bh = new BaseHeader(); 272 bh.setAsciiEncoding (ASCII_ENCODING.equalsIgnoreCase(atts.getValue(ENCODING_ATTR))); 273 stk.push (bh); 274 } 275 } catch (ISOException e) { 276 throw new SAXException ("ISOException unpacking XML dataset", e); 277 } 278 } 279 280 public void characters (char ch[], int start, int length) { 281 Object obj = stk.peek(); 282 if (obj instanceof ISOField) { 283 ISOField f = (ISOField) obj; 284 String value = f.getValue() + new String(ch, start, length); 285 try { 286 f.setValue(value); 287 } catch (ISOException e) { 288 try { 289 f.setValue (e.getMessage()); 290 } catch (ISOException ignored) { 291 // giving up 292 } 293 } 294 } 295 else if (obj instanceof BaseHeader) { 296 BaseHeader bh = (BaseHeader) obj; 297 String s = new String(ch,start,length); 298 if (bh.isAsciiEncoding()) { 299 bh.unpack (s.getBytes()); 300 } else { 301 bh.unpack (ISOUtil.hex2byte (s)); 302 } 303 } 304 } 305 306 public void endElement (String ns, String name, String qname) 307 throws SAXException 308 { 309 if (name.equals (ISOMSG_TAG)) { 310 ISOMsg m = (ISOMsg) stk.pop(); 311 if (stk.empty()) 312 stk.push (m); // push outer message 313 } else if (DATASET_TAG.equals(name)) { 314 stk.pop(); 315 } else if (ISOFIELD_TAG.equals (name)) { 316 stk.pop(); 317 } else if (HEADER_TAG.equals (name)) { 318 BaseHeader h = (BaseHeader) stk.pop(); 319 ISOMsg m = (ISOMsg) stk.peek (); 320 m.setHeader (h); 321 } 322 } 323 324 // we may want to force fome fields to be interpreted as binary data 325 /** 326 * Converts selected message fields from hexadecimal strings into binary values. 327 * 328 * @param m message being adjusted 329 * @param bfields field numbers to convert 330 * @throws ISOException if any field cannot be converted 331 */ 332 protected void fixupBinary(ISOMsg m, int[] bfields) throws ISOException { 333 if (bfields != null) { 334 for (int f : bfields) { 335 if (m.hasField(f)) { 336 ISOComponent c = m.getComponent(f); 337 if (c instanceof ISOField) 338 m.set(f, ((ISOField) c).getBytes()); 339 } 340 } 341 } 342 } 343 344 public String getFieldDescription(ISOComponent m, int fldNumber) { 345 return "Data element " + fldNumber; 346 } 347 public void setLogger (Logger logger, String realm) { 348 this.logger = logger; 349 this.realm = realm; 350 } 351 public String getRealm () { 352 return realm; 353 } 354 public Logger getLogger() { 355 return logger; 356 } 357 public ISOMsg createISOMsg () { 358 return new ISOMsg(); 359 } 360 public String getDescription () { 361 return getClass().getName(); 362 } 363 364 /** 365 * Creates the SAX reader used to parse XML ISO messages. 366 * 367 * @return configured XML reader instance 368 * @throws SAXException if the reader cannot be created 369 */ 370 protected XMLReader createXMLReader () throws SAXException { 371 XMLReader reader; 372 try { 373 reader = XMLReaderFactory.createXMLReader(); 374 } catch (SAXException e) { 375 reader = XMLReaderFactory.createXMLReader ( 376 System.getProperty( 377 "org.xml.sax.driver", 378 "org.apache.crimson.parser.XMLReaderImpl" 379 ) 380 ); 381 } 382 383 reader.setContentHandler(this); 384 reader.setErrorHandler(this); 385 return reader; 386 } 387 388 /** 389 * Sets a SAX feature on the underlying XML parser. 390 * 391 * @param fname feature name URI 392 * @param val feature value to apply 393 * @throws SAXException if the parser rejects the feature 394 */ 395 public void setXMLParserFeature(String fname, boolean val) throws SAXException { 396 reader.setFeature(fname, val); 397 } 398 399 private int parseDecimalId(String id) throws SAXException { 400 if (id == null) 401 return -1; 402 try { 403 return Integer.parseInt(id); 404 } catch (NumberFormatException ex) { 405 throw new SAXException("Invalid id " + id, ex); 406 } 407 } 408 409 private int parseDecimalOrHexId(String id) throws SAXException { 410 try { 411 return id.startsWith("0x") || id.startsWith("0X") ? Integer.parseInt(id.substring(2), 16) : Integer.parseInt(id); 412 } catch (NumberFormatException ex) { 413 throw new SAXException("Invalid id " + id, ex); 414 } 415 } 416 417 private int parseHexId(String id) throws SAXException { 418 try { 419 String normalized = id.startsWith("0x") || id.startsWith("0X") ? id.substring(2) : id; 420 return Integer.parseInt(normalized, 16); 421 } catch (NumberFormatException ex) { 422 throw new SAXException("Invalid hex id " + id, ex); 423 } 424 } 425 426 private boolean isConstructedTag(int tag) { 427 String hexTag = Integer.toHexString(tag); 428 if ((hexTag.length() & 0x01) == 1) 429 hexTag = "0" + hexTag; 430 byte[] tagBytes = ISOUtil.hex2byte(hexTag); 431 return (tagBytes[0] & 0x20) == 0x20; 432 } 433}