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 * &lt;isopackager&gt;
053 *     &lt;isofield
054 *         id="[field id]"
055 *         name="[field name]"
056 *         length="[max field length]"
057 *         class="[org.jpos.iso.IF_*]"
058 *         pad="true|false"&gt;
059 *     &lt;/isofield&gt;
060 *     ...
061 * &lt;/isopackager&gt;
062 *
063 * Fields that contain subfields can be handled as follows
064 * &lt;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.*]"&gt;
070 *
071 *     &lt;isofield
072 *         id="[subfield id]"
073 *         name="[subfield name]"
074 *         length="[max subfield length]"
075 *         class="[org.jpos.iso.IF_*]"
076 *         pad="true|false"&gt;
077 *     &lt;/isofield&gt;
078 *         ...
079 * &lt;/isofieldpackager&gt;
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}