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 protected Logger logger = null; 052 protected String realm = null; 053 private XMLReader reader; 054 private Stack stk; 055 private Lock parserLock = new ReentrantLock(); 056 057 public static final String ISOMSG_TAG = "isomsg"; 058 public static final String ISOFIELD_TAG = "field"; 059 public static final String ID_ATTR = "id"; 060 public static final String VALUE_ATTR = "value"; 061 public static final String TYPE_ATTR = "type"; 062 public static final String TYPE_BINARY = "binary"; 063 public static final String TYPE_BITMAP = "bitmap"; 064 public static final String TYPE_AMOUNT = "amount"; 065 public static final String CURRENCY_ATTR = "currency"; 066 public static final String HEADER_TAG = "header"; 067 public static final String ENCODING_ATTR = "encoding"; 068 public static final String ASCII_ENCODING= "ascii"; 069 070 // fields that will be forced to be interpreted as binary data 071 private int[] binaryFields= null; 072 073 public XMLPackager() throws ISOException { 074 super(); 075 stk = new Stack(); 076 try { 077 reader = createXMLReader(); 078 079 // some parser restrictions have been set for security and maybe PCI compliance 080 setXMLParserFeature("http://xml.org/sax/features/validation", false); 081 setXMLParserFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 082 setXMLParserFeature("http://xml.org/sax/features/external-general-entities", false); 083 setXMLParserFeature("http://xml.org/sax/features/external-parameter-entities", false); 084 } catch (Exception e) { 085 throw new ISOException (e.toString()); 086 } 087 } 088 089 public void forceBinary(int ... bfields) { 090 binaryFields= bfields; 091 } 092 093 public byte[] pack (ISOComponent c) throws ISOException { 094 LogEvent evt = new LogEvent (this, "pack"); 095 096 try { 097 if (!(c instanceof ISOMsg m)) 098 throw new ISOException ("cannot pack "+c.getClass()); 099 100 // heuristics for initial size 40: a typical <field> with 2 chars of id + 12 of value + 2 indent 101 ByteArrayOutputStream out = new ByteArrayOutputStream(40 * (m.getChildren().size()+2)); 102 PrintStream p = new PrintStream(out, false, StandardCharsets.UTF_8); 103 104 m.setDirection(0); // avoid "direction=xxxxxx" in XML msg 105 m.dump (p, ""); 106 byte[] b = out.toByteArray(); 107 108 if (logger != null) 109 evt.addMessage (m); 110 return b; 111 } catch (ISOException e) { 112 evt.addMessage (e); 113 throw e; 114 } finally { 115 Logger.log(evt); 116 } 117 } 118 119 public int unpack (ISOComponent c, byte[] b) throws ISOException { 120 unpack(c, new InputSource(new ByteArrayInputStream(b))); 121 return b.length; 122 } 123 124 public void unpack (ISOComponent c, InputStream in) throws ISOException, IOException { 125 unpack(c, new InputSource(in)); 126 } 127 128 private void unpack (ISOComponent c, InputSource in) throws ISOException { 129 LogEvent evt = new LogEvent (this, "unpack"); 130 131 parserLock.lock(); 132 try { 133 if (!(c instanceof ISOMsg m)) 134 throw new ISOException("Can't call packager on non Composite"); 135 136 while (!stk.empty()) // purge from possible previous error 137 stk.pop(); 138 139 reader.parse (in); 140 if (stk.empty()) 141 throw new ISOException ("error parsing"); 142 143 ISOMsg m1 = (ISOMsg) stk.pop(); 144 m.merge (m1); 145 m.setHeader (m1.getHeader()); 146 147 fixupBinary(m, binaryFields); 148 149 if (logger != null) 150 evt.addMessage (m); 151 } catch (ISOException e) { 152 evt.addMessage (e); 153 throw e; 154 } catch (IOException e) { 155 evt.addMessage (e); 156 throw new ISOException (e.toString()); 157 } catch (SAXException e) { 158 evt.addMessage (e); 159 throw new ISOException (e.toString()); 160 } finally { 161 Logger.log (evt); 162 parserLock.unlock(); 163 } 164 } 165 166 public void startElement 167 (String ns, String name, String qName, Attributes atts) 168 throws SAXException 169 { 170 int fieldNumber = -1; 171 try { 172 String id = atts.getValue(ID_ATTR); 173 if (id != null) { 174 try { 175 fieldNumber = Integer.parseInt (id); 176 } catch (NumberFormatException ex) { 177 throw new SAXException ("Invalid id " + id); 178 } 179 } 180 if (name.equals (ISOMSG_TAG)) { 181 if (fieldNumber >= 0) { 182 if (stk.empty()) 183 throw new SAXException ("inner without outer"); 184 185 ISOMsg inner = new ISOMsg(fieldNumber); 186 ((ISOMsg)stk.peek()).set (inner); 187 stk.push (inner); 188 } else { 189 stk.push (new ISOMsg(0)); 190 } 191 } else if (name.equals (ISOFIELD_TAG)) { 192 ISOMsg m = (ISOMsg) stk.peek(); 193 String value = atts.getValue(VALUE_ATTR); 194 String type = atts.getValue(TYPE_ATTR); 195 if (id == null) 196 throw new SAXException ("invalid field"); 197 value = value == null ? "" : value; 198 199 ISOComponent ic; 200 if (TYPE_BINARY.equals (type)) { 201 ic = new ISOBinaryField ( 202 fieldNumber, 203 ISOUtil.hex2byte ( 204 value.getBytes(), 0, value.length()/2 205 ) 206 ); 207 } 208 else if (TYPE_AMOUNT.equals (type)) { 209 ic = new ISOAmount( 210 fieldNumber, 211 Integer.parseInt (atts.getValue(CURRENCY_ATTR)), 212 new BigDecimal (value) 213 ); 214 } 215 else { 216 ic = new ISOField (fieldNumber, ISOUtil.stripUnicode(value)); 217 } 218 m.set (ic); 219 stk.push (ic); 220 } else if (HEADER_TAG.equals (name)) { 221 BaseHeader bh = new BaseHeader(); 222 bh.setAsciiEncoding (ASCII_ENCODING.equalsIgnoreCase(atts.getValue(ENCODING_ATTR))); 223 stk.push (bh); 224 } 225 } catch (ISOException e) { 226 throw new SAXException 227 ("ISOException unpacking "+fieldNumber); 228 } 229 } 230 231 public void characters (char ch[], int start, int length) { 232 Object obj = stk.peek(); 233 if (obj instanceof ISOField) { 234 ISOField f = (ISOField) obj; 235 String value = f.getValue() + new String(ch, start, length); 236 try { 237 f.setValue(value); 238 } catch (ISOException e) { 239 try { 240 f.setValue (e.getMessage()); 241 } catch (ISOException ignored) { 242 // giving up 243 } 244 } 245 } 246 else if (obj instanceof BaseHeader) { 247 BaseHeader bh = (BaseHeader) obj; 248 String s = new String(ch,start,length); 249 if (bh.isAsciiEncoding()) { 250 bh.unpack (s.getBytes()); 251 } else { 252 bh.unpack (ISOUtil.hex2byte (s)); 253 } 254 } 255 } 256 257 public void endElement (String ns, String name, String qname) 258 throws SAXException 259 { 260 if (name.equals (ISOMSG_TAG)) { 261 ISOMsg m = (ISOMsg) stk.pop(); 262 if (stk.empty()) 263 stk.push (m); // push outer message 264 } else if (ISOFIELD_TAG.equals (name)) { 265 stk.pop(); 266 } else if (HEADER_TAG.equals (name)) { 267 BaseHeader h = (BaseHeader) stk.pop(); 268 ISOMsg m = (ISOMsg) stk.peek (); 269 m.setHeader (h); 270 } 271 } 272 273 // we may want to force fome fields to be interpreted as binary data 274 protected void fixupBinary(ISOMsg m, int[] bfields) throws ISOException { 275 if (bfields != null) { 276 for (int f : bfields) { 277 if (m.hasField(f)) { 278 ISOComponent c = m.getComponent(f); 279 if (c instanceof ISOField) 280 m.set(f, ((ISOField) c).getBytes()); 281 } 282 } 283 } 284 } 285 286 public String getFieldDescription(ISOComponent m, int fldNumber) { 287 return "Data element " + fldNumber; 288 } 289 public void setLogger (Logger logger, String realm) { 290 this.logger = logger; 291 this.realm = realm; 292 } 293 public String getRealm () { 294 return realm; 295 } 296 public Logger getLogger() { 297 return logger; 298 } 299 public ISOMsg createISOMsg () { 300 return new ISOMsg(); 301 } 302 public String getDescription () { 303 return getClass().getName(); 304 } 305 306 protected XMLReader createXMLReader () throws SAXException { 307 XMLReader reader; 308 try { 309 reader = XMLReaderFactory.createXMLReader(); 310 } catch (SAXException e) { 311 reader = XMLReaderFactory.createXMLReader ( 312 System.getProperty( 313 "org.xml.sax.driver", 314 "org.apache.crimson.parser.XMLReaderImpl" 315 ) 316 ); 317 } 318 319 reader.setContentHandler(this); 320 reader.setErrorHandler(this); 321 return reader; 322 } 323 324 public void setXMLParserFeature(String fname, boolean val) throws SAXException { 325 reader.setFeature(fname, val); 326 } 327} 328