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