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