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.core.Configurable; 022import org.jpos.core.Configuration; 023import org.jpos.core.ConfigurationException; 024import org.jpos.iso.*; 025import org.jpos.util.LogSource; 026import org.jpos.util.Logger; 027import org.xml.sax.Attributes; 028import org.xml.sax.EntityResolver; 029import org.xml.sax.InputSource; 030import org.xml.sax.SAXException; 031import org.xml.sax.SAXParseException; 032import org.xml.sax.XMLReader; 033import org.xml.sax.helpers.DefaultHandler; 034import org.xml.sax.helpers.XMLReaderFactory; 035 036import java.io.File; 037import java.io.FileInputStream; 038import java.io.IOException; 039import java.io.InputStream; 040import java.lang.reflect.InvocationTargetException; 041import java.net.URL; 042import java.util.Map; 043import java.util.Map.Entry; 044import java.util.Stack; 045import java.util.TreeMap; 046 047 048/** 049 * <pre> 050 * GenericPackager uses an XML config file to describe the layout of an ISOMessage 051 * The general format is as follows 052 * <isopackager> 053 * <isofield 054 * id="[field id]" 055 * name="[field name]" 056 * length="[max field length]" 057 * class="[org.jpos.iso.IF_*]" 058 * pad="true|false"> 059 * </isofield> 060 * ... 061 * </isopackager> 062 * 063 * Fields that contain subfields can be handled as follows 064 * <isofieldpackager 065 * id="[field id]" 066 * name="[field name]" 067 * length="[field length]" 068 * class="[org.jpos.iso.IF_*]" 069 * packager="[org.jpos.iso.packager.*]"> 070 * 071 * <isofield 072 * id="[subfield id]" 073 * name="[subfield name]" 074 * length="[max subfield length]" 075 * class="[org.jpos.iso.IF_*]" 076 * pad="true|false"> 077 * </isofield> 078 * ... 079 * </isofieldpackager> 080 * 081 * The optional attributes maxValidField, bitmapField, thirdBitmapField, and emitBitmap 082 * are allowed on the isopackager node. 083 * 084 * </pre> 085 * @author Eoin Flood 086 * @version $Revision$ $Date$ 087 * @see ISOPackager 088 * @see ISOBasePackager 089 */ 090 091@SuppressWarnings("unchecked") 092public class GenericPackager 093 extends ISOBasePackager implements Configurable, GenericPackagerParams 094{ 095 /* Values copied from ISOBasePackager 096 These can be changes using attributes on the isopackager node */ 097 private int maxValidField=128; 098 private boolean emitBitmap=true; 099 private int bitmapField=1; 100 private String firstField = null; 101 private String filename; 102 103 public GenericPackager() throws ISOException 104 { 105 super(); 106 } 107 108 /** 109 * Create a GenericPackager with the field descriptions 110 * from an XML File 111 * @param filename The XML field description file 112 */ 113 public GenericPackager(String filename) throws ISOException 114 { 115 this.filename = filename; 116 readFile(filename); 117 } 118 119 /** 120 * Create a GenericPackager with the field descriptions 121 * from an XML InputStream 122 * @param input The XML field description InputStream 123 */ 124 public GenericPackager(InputStream input) throws ISOException 125 { 126 readFile(input); 127 } 128 129 /** 130 * Packager Configuration. 131 * 132 * <ul> 133 * <li>packager-config 134 * <li>packager-logger 135 * <li>packager-log-fieldname 136 * <li>packager-realm 137 * </ul> 138 * 139 * @param cfg Configuration 140 */ 141 public void setConfiguration (Configuration cfg) 142 throws ConfigurationException 143 { 144 filename = cfg.get("packager-config", null); 145 if (filename == null) 146 throw new ConfigurationException("packager-config property cannot be null"); 147 148 try 149 { 150 String loggerName = cfg.get("packager-logger", null); 151 if (loggerName != null) 152 setLogger(Logger.getLogger (loggerName), 153 cfg.get ("packager-realm")); 154 155 // inherited protected logFieldName 156 logFieldName= cfg.getBoolean("packager-log-fieldname", logFieldName); 157 158 readFile(filename); 159 } catch (ISOException e) 160 { 161 throw new ConfigurationException(e.getMessage(), e.fillInStackTrace()); 162 } 163 } 164 165 @Override 166 protected int getMaxValidField() 167 { 168 return maxValidField; 169 } 170 171 @Override 172 protected boolean emitBitMap() 173 { 174 return emitBitmap; 175 } 176 177 @Override 178 protected ISOFieldPackager getBitMapfieldPackager() 179 { 180 return fld[bitmapField]; 181 } 182 183 /** 184 * Parse the field descriptions from an XML file. 185 * 186 * <pre> 187 * Uses the sax parser specified by the system property 'sax.parser' 188 * The default parser is org.apache.crimson.parser.XMLReaderImpl 189 * </pre> 190 * @param filename The XML field description file 191 */ 192 public void readFile(String filename) throws ISOException 193 { 194 try { 195 if (filename.startsWith("jar:") && filename.length()>4) { 196 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 197 readFile( 198 cl.getResourceAsStream(filename.substring(4)) 199 ); 200 } else { 201 createXMLReader().parse(filename); 202 } 203 } 204 catch (Exception e) { 205 throw new ISOException("Error reading " + filename, e); 206 } 207 } 208 209 /** 210 * Parse the field descriptions from an XML InputStream. 211 * 212 * <pre> 213 * Uses the sax parser specified by the system property 'sax.parser' 214 * The default parser is org.apache.crimson.parser.XMLReaderImpl 215 * </pre> 216 * @param input The XML field description InputStream 217 */ 218 public void readFile(InputStream input) throws ISOException 219 { 220 try { 221 createXMLReader().parse(new InputSource(input)); 222 } 223 catch (Exception e) { 224 throw new ISOException(e); 225 } 226 } 227 @Override 228 public void setLogger (Logger logger, String realm) { 229 super.setLogger (logger, realm); 230 if (fld != null) { 231 for (int i=0; i<fld.length; i++) { 232 if (fld[i] instanceof ISOMsgFieldPackager) { 233 Object o = ((ISOMsgFieldPackager)fld[i]).getISOMsgPackager(); 234 if (o instanceof LogSource) { 235 ((LogSource)o).setLogger (logger, realm + "-fld-" + i); 236 } 237 } 238 } 239 } 240 } 241 private XMLReader createXMLReader () throws SAXException { 242 XMLReader reader; 243 try { 244 reader = XMLReaderFactory.createXMLReader(); 245 } catch (SAXException e) { 246 reader = XMLReaderFactory.createXMLReader ( 247 System.getProperty( 248 "org.xml.sax.driver", 249 "org.apache.crimson.parser.XMLReaderImpl" 250 ) 251 ); 252 } 253 reader.setFeature ("http://xml.org/sax/features/validation", true); 254 GenericContentHandler handler = new GenericContentHandler(); 255 reader.setContentHandler(handler); 256 reader.setErrorHandler(handler); 257 reader.setEntityResolver(new GenericEntityResolver()); 258 return reader; 259 } 260 @Override 261 public String getDescription () { 262 StringBuilder sb = new StringBuilder(); 263 sb.append (super.getDescription()); 264 if (filename != null) { 265 sb.append ('['); 266 sb.append (filename); 267 sb.append (']'); 268 } 269 return sb.toString(); 270 } 271 272 @Override 273 public void setGenericPackagerParams (Attributes atts) { 274 String maxField = atts.getValue("maxValidField"); 275 String emitBmap = atts.getValue("emitBitmap"); 276 String bmapfield = atts.getValue("bitmapField"); 277 String thirdbmf = atts.getValue("thirdBitmapField"); 278 firstField = atts.getValue("firstField"); 279 String headerLenStr = atts.getValue("headerLength"); 280 281 if (maxField != null) 282 maxValidField = Integer.parseInt(maxField); 283 284 if (emitBmap != null) 285 emitBitmap = Boolean.valueOf(emitBmap); 286 287 if (bmapfield != null) 288 bitmapField = Integer.parseInt(bmapfield); 289 290 // BBB TODO IDEA: should we check somewhere that fld[thirdBitmapField] instanceof ISOBitMapPackager? 291 if (thirdbmf != null) 292 try { setThirdBitmapField(Integer.parseInt(thirdbmf)); } 293 catch (ISOException e) 294 { // BBB throwing unchecked exception in order not to change the method's contract 295 // BBB (the parseInt's and valueOf's above are doing it anyway...) 296 throw new IllegalArgumentException(e.getMessage()); 297 } 298 299 if (firstField != null) 300 Integer.parseInt (firstField); // attempt to parse just to 301 // force an exception if the 302 // data is not correct. 303 if (headerLenStr != null) 304 setHeaderLength(Integer.parseInt(headerLenStr)); 305 } 306 307 public static class GenericEntityResolver implements EntityResolver 308 { 309 /** 310 * Allow the application to resolve external entities. 311 * <p/> 312 * The strategy we follow is:<p> 313 * We first check whether the DTD points to a well defined URI, 314 * and resolve to our internal DTDs.<p> 315 * 316 * If the systemId points to a file, then we attempt to read the 317 * DTD from the filesystem, in case they've been modified by the user. 318 * Otherwise, we fallback to the built-in DTDs inside jPOS.<p> 319 * 320 * @param publicId The public identifier of the external entity 321 * being referenced, or null if none was supplied. 322 * @param systemId The system identifier of the external entity 323 * being referenced. 324 * @return An InputSource object describing the new input source, 325 * or null to request that the parser open a regular 326 * URI connection to the system identifier. 327 * @throws org.xml.sax.SAXException Any SAX exception, possibly 328 * wrapping another exception. 329 * @throws java.io.IOException A Java-specific IO exception, 330 * possibly the result of creating a new InputStream 331 * or Reader for the InputSource. 332 * @see org.xml.sax.InputSource 333 */ 334 public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException 335 { 336 if(systemId==null) return null; 337 338 ClassLoader cl =Thread.currentThread().getContextClassLoader(); 339 cl = cl ==null?ClassLoader.getSystemClassLoader() : cl; 340 341 if(systemId.equals("http://jpos.org/dtd/generic-packager-1.0.dtd")) 342 { 343 final URL resource = cl.getResource("org/jpos/iso/packager/genericpackager.dtd"); 344 return new InputSource(resource.toExternalForm()); 345 } 346 else if(systemId.equals("http://jpos.org/dtd/generic-validating-packager-1.0.dtd")) 347 { 348 final URL resource = cl.getResource("org/jpos/iso/packager/generic-validating-packager.dtd"); 349 return new InputSource(resource.toExternalForm()); 350 } 351 352 URL url=new URL(systemId); 353 if(url.getProtocol().equals("file")) 354 { 355 String file=url.getFile(); 356 if(file.endsWith(".dtd")) 357 { 358 File f=new File(file); 359 InputStream res=null; 360 if(f.exists()) 361 { 362 res=new FileInputStream(f); 363 } 364 if(res==null) 365 { 366 String dtdResource="org/jpos/iso/packager/"+f.getName(); 367 res= cl.getResourceAsStream(dtdResource); 368 } 369 if(res!=null) return new InputSource(res); 370 } 371 } 372 return null; 373 } 374 } 375 376 public class GenericContentHandler extends DefaultHandler 377 { 378 private Stack<Object> fieldStack; 379 380 @Override 381 public void startDocument() 382 { 383 fieldStack = new Stack<Object>(); 384 } 385 386 @Override 387 public void endDocument() throws SAXException 388 { 389 if (!fieldStack.isEmpty()) 390 { 391 throw new SAXException ("Format error in XML Field Description File"); 392 } 393 } 394 395 @Override 396 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) 397 throws SAXException 398 { 399 try 400 { 401 String id = atts.getValue("id"); 402 String type = atts.getValue("class"); 403 String name = atts.getValue("name"); 404 String size = atts.getValue("length"); 405 String pad = atts.getValue("pad"); 406 // Modified for using TaggedFieldPackager 407 String token = atts.getValue("token"); 408 String trim = atts.getValue("trim"); 409 String params = atts.getValue("params"); 410 411 if (localName.equals("isopackager")) 412 { 413 // Stick a new Map on stack to collect the fields 414 fieldStack.push(new TreeMap()); 415 416 setGenericPackagerParams (atts); 417 } 418 419 if (localName.equals("isofieldpackager")) 420 { 421 /* 422 For an isofield packager node push the following fields 423 onto the stack. 424 1) an Integer indicating the field ID 425 2) an instance of the specified ISOFieldPackager class 426 3) an instance of the specified ISOBasePackager (msgPackager) class 427 4) a Map to collect the subfields 428 */ 429 String packager = atts.getValue("packager"); 430 431 fieldStack.push(Integer.valueOf(id)); 432 433 ISOFieldPackager f; 434 f = (ISOFieldPackager) Class.forName(type).newInstance(); 435 f.setDescription(name); 436 f.setLength(Integer.parseInt(size)); 437 f.setPad(Boolean.parseBoolean(pad)); 438 if (f instanceof GenericPackagerParams) 439 ((GenericPackagerParams)f).setGenericPackagerParams (atts); 440 441 // Modified for using TaggedFieldPackager 442 if( f instanceof TaggedFieldPackager){ 443 ((TaggedFieldPackager)f).setToken( token ); 444 } 445 fieldStack.push(f); 446 447 ISOBasePackager p = (ISOBasePackager) instantiate(packager, params); 448 if (p instanceof GenericPackagerParams) 449 ((GenericPackagerParams)p).setGenericPackagerParams (atts); 450 fieldStack.push(p); 451 452 fieldStack.push(new TreeMap()); 453 } 454 else if (localName.equals("isofield")) 455 { 456 Class c = Class.forName(type); 457 ISOFieldPackager f; 458 f = (ISOFieldPackager) instantiate(type, params); 459 f.setDescription(name); 460 f.setLength(Integer.parseInt(size)); 461 f.setPad(Boolean.parseBoolean(pad)); 462 f.setTrim(Boolean.parseBoolean(trim)); 463 // Modified for using TaggedFieldPackager 464 if( f instanceof TaggedFieldPackager){ 465 ((TaggedFieldPackager)f).setToken( token ); 466 } 467 // Insert this new isofield into the Map 468 // on the top of the stack using the fieldID as the key 469 Map m = (Map) fieldStack.peek(); 470 m.put(Integer.valueOf(id), f); 471 } 472 } 473 catch (Exception ex) 474 { 475 throw new SAXException(ex); 476 } 477 } 478 479 /** 480 * Convert the ISOFieldPackagers in the Map 481 * to an array of ISOFieldPackagers 482 */ 483 private ISOFieldPackager[] makeFieldArray(Map<Integer,ISOFieldPackager> m) 484 { 485 int maxField = 0; 486 487 // First find the largest field number in the Map 488 for (Entry<Integer,ISOFieldPackager> ent :m.entrySet()) 489 if (ent.getKey() > maxField) 490 maxField = ent.getKey(); 491 492 // Create the array 493 ISOFieldPackager fld[] = new ISOFieldPackager[maxField+1]; 494 495 // Populate it 496 for (Entry<Integer,ISOFieldPackager> ent :m.entrySet()) 497 fld[ent.getKey()] = ent.getValue(); 498 return fld; 499 } 500 501 @Override 502 public void endElement(String namespaceURI, String localName, String qName) 503 { 504 Map<Integer,ISOFieldPackager> m; 505 if (localName.equals("isopackager")) 506 { 507 m = (Map)fieldStack.pop(); 508 509 setFieldPackager(makeFieldArray(m)); 510 } 511 512 if (localName.equals("isofieldpackager")) 513 { 514 // Pop the 4 entries off the stack in the correct order 515 m = (Map)fieldStack.pop(); 516 517 ISOBasePackager msgPackager = (ISOBasePackager) fieldStack.pop(); 518 msgPackager.setFieldPackager (makeFieldArray(m)); 519 520 ISOFieldPackager fieldPackager = (ISOFieldPackager) fieldStack.pop(); 521 522 Integer fno = (Integer) fieldStack.pop(); 523 524 msgPackager.setLogger (getLogger(), getRealm() + "-fld-" + fno); 525 526 // Create the ISOMsgField packager with the retrieved msg and field Packagers 527 ISOMsgFieldPackager mfp = 528 new ISOMsgFieldPackager(fieldPackager, msgPackager); 529 530 // Add the newly created ISOMsgField packager to the 531 // lower level field stack 532 533 m=(Map)fieldStack.peek(); 534 m.put(fno, mfp); 535 } 536 } 537 538 // ErrorHandler Methods 539 @Override 540 public void error (SAXParseException ex) throws SAXException 541 { 542 throw ex; 543 } 544 545 @Override 546 public void fatalError (SAXParseException ex) throws SAXException 547 { 548 throw ex; 549 } 550 } 551 @Override 552 protected int getFirstField() { 553 if (firstField != null) 554 return Integer.parseInt (firstField); 555 else return super.getFirstField(); 556 } 557 558 /** 559 * Helper class used to instantiate packagers 560 * 561 * @param clazz class name 562 * @param params If not null <code>constructor(String)</code> has to exist in packager implementation. 563 * 564 * @return newly created object 565 * @throws ClassNotFoundException 566 * @throws NoSuchMethodException 567 * @throws IllegalAccessException 568 * @throws InstantiationException 569 */ 570 private Object instantiate (String clazz, String params) 571 throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { 572 Object obj; 573 if (params != null) 574 obj = Class.forName(clazz).getConstructor(String.class).newInstance(params); 575 else 576 obj = Class.forName(clazz).newInstance(); 577 578 return obj; 579 } 580}