/* * eGov suite of products aim to improve the internal efficiency,transparency, * accountability and the service delivery of the government organizations. * * Copyright (C) <2015> eGovernments Foundation * * The updated version of eGov suite of products as by eGovernments Foundation * is available at http://www.egovernments.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/ or * http://www.gnu.org/licenses/gpl.html . * * In addition to the terms of the GPL license to be adhered to in using this * program, the following additional terms are to be complied with: * * 1) All versions of this program, verbatim or modified must carry this * Legal Notice. * * 2) Any misrepresentation of the origin of the material is prohibited. It * is required that all modified versions of this material be marked in * reasonable ways as different from the original version. * * 3) This license does not grant any rights to any user of the program * with regards to rights under trademark law for use of the trade names * or trademarks of eGovernments Foundation. * * In case of any queries, you can reach eGovernments Foundation at contact@egovernments.org. */ package com.exilant.exility.common; import org.apache.log4j.Logger; import org.xml.sax.Attributes; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Stack; /** * @author raghu.bhandi * * XMLLoader loads an object from an XML source file. It can load a complete object map from a corresponding XML file * * 1. XML uses only attributes of an node. e.g. <type> or <type attr1="vaal1" attr2=Val2 /> 2. Values of the type <tag>Some text * value of tag</tag> are not used. If found, they are ignored 3. Hierarchies are possible. e.g. <group attr1="val1" attr2="val2"> * <member1 attr="val" junk="junk"/> .... </group> * * For an object to be 'loadable' follow these guidelines. (No interface is defined as these are naming conventions, and not * methods) 1. For each possible atribute, define a field (data member) of appropriate data type. primitive data types, and string * are the only ones to be used. 2. Alternately, you can define a HashMap named 'attributes'. All attributes, for which there is * no corresponding field name, are stored in this hashmap as name/value pairs. 3. If the class/object can have children (child * nodes) you should plan to load them as well 4. Note that each node in the XML is loaded into one object by default. Each node * will have a corresponding class 5. If a child node occurs only once, you can define a field for that with the name matching the * tag name. Loader will load the child node into this object. If you just declare it without instantiatiing it, Loader tries teh * defalt constructor to iinstantiate it 6. If a type of child node occurs more than once, you can either plan to store them in an * array or a HashMap If you need to locate them using key, then HashMap is the preferred way, while array is effecient. You can * also store them in ArrayList if you have to add child nodes later (after loader loads them) Loader looks for a field with the * name =tagname + 's' . For e.g. <main > <child id="a1" someattribute="value"/> <child id="a2" someattribute="somevalue"/> * </main> in this case, loader looks for a field with name childs and tries to load all child nodes into this field 7. If array * is defined, Loader can determine the calss (Type) of child obbjects. However, if it is HashMap or ArrayList, Loader can not * figure out the calss (type) to be used to instantiate teh child object. You have to declare a field with the name of the tag * for this. For example, for the above xml, define ChildClass child; HashMap children; 8. The key for HashMap: attribute with * name id, key, and name are searched, in that order and used as key for HashMap. 9. If your needs to store child objects are * more complex than this, then you can write your own method. a method newChild(String tag, String key) is called by the loader * whenever a child node is encountered you can instantiate a child object, store it appropriately, and return the reference for * the loader to load it. Loader calls endChild(String tag) when the end tag is encountered. This method is optional and you need * not provide this. It is called only if it is defined. 10. Tag name of the node is stored in a String field with name as 'node' * if such a field is defined for the object (note: if you also have an attribute with name as 'type', the attribute value * over-rides the tag name) 11. If any field is of type boolean, then you should specify "true", "yes" or "1" as the value for * true. else it is considered to be false; * */ public class XMLLoader extends DefaultHandler { /* * Each open node is pushed to the stack. Information about each type of child nodes is stored and saved along with the object * in teh stack. Look at StackedObject and ChildInfo classes for details */ Stack openNodes; // keeps track of open nodes in the XML. Object rootObject; // object to which the XML is to be loaded private static final Logger LOGGER = Logger.getLogger(XMLLoader.class); public XMLLoader() { } public void load(final String fileName, final Object root) { rootObject = root; openNodes = new Stack(); load(fileName); rootObject = null; openNodes = null; } /************************************** * * @param fileName Uses a SAXParser to parse and load the XML into the object that was set using setObject() method * ***************************************/ private void load(final String fileName) { try { final SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setNamespaceAware(false); // Validation part 1: set whether validation is on spf.setValidating(false); // Create a JAXP SAXParser final SAXParser saxParser = spf.newSAXParser(); // Get the encapsulated SAX XMLReader final XMLReader xmlReader = saxParser.getXMLReader(); // Set the ContentHandler of the XMLReader xmlReader.setContentHandler(this); // Set an ErrorHandler before parsing. This error handler is // defifined as a subClass in this class xmlReader.setErrorHandler(new MyErrorHandler(System.err)); // Tell the XMLReader to parse the XML document xmlReader.parse(fileName); } catch (final ParserConfigurationException e) { LOGGER.error("Exp=" + e.getMessage()); } catch (final SAXException e) { LOGGER.error("Exp=" + e.getMessage()); } catch (final IOException e) { LOGGER.error("Exp=" + e.getMessage()); } finally { if (LOGGER.isInfoEnabled()) LOGGER.info("Finally in load"); } } /********************************** * Call back methods used by SaxLoader. startElement() and endElement() */ /* * Start element is called by the parser whenever the beginning tag is parsed Our assumption is that the tags contian only * attribute, and no text value. i.e we do not expect a case like <type>Some value </type> qName is the tag name, and atts * contains all name/value pairs of attribute say <tag name1="value1" name2="value2"... * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) */ @Override public void startElement(final String namespaceURI, final String localName, final String qName, final Attributes atts) throws SAXException { Object objectToLoad = null; if (openNodes.size() == 0) // node objectToLoad = rootObject; // load the root node to root object else objectToLoad = createChild(qName, atts); if (objectToLoad != null) { // if it is null, the node is not loaded at // all.. if (atts.getIndex("type") < 0) ObjectGetSetter.set(objectToLoad, "type", qName); // try saving // tag as // 'type' // attribute ObjectGetSetter.setAll(objectToLoad, atts); // set all attributes // from atts to members // of childobject } openNodes.push(new StackedObject(objectToLoad)); } /********************** * endElement() is called by the parser when the /> or </element> is encountered */ @Override public void endElement(final String namespaceURI, final String localName, final String qName) throws SAXException { final StackedObject so = (StackedObject) openNodes.pop(); final Object endingObject = so.object; if (endingObject == null) return; // Call endChild() for all of the childInfo() that we may have if (so.childInfos != null && so.childInfos.size() > 0) { final Iterator iter = so.childInfos.values().iterator(); while (iter.hasNext()) ((ChildInfo) iter.next()).endChild(endingObject); } } /* * Whenevr a child node is detected by the parser, a new Object is to be supplied to set all the attributes. This helper class * tries different conventions to create a child object */ private Object createChild(final String tag, final Attributes atts) { if (openNodes.size() == 0) return null; final StackedObject stackedObject = (StackedObject) openNodes.peek(); final Object parentObject = stackedObject.object; if (null == stackedObject.childInfos) stackedObject.childInfos = new HashMap(); ChildInfo childInfo = null; if (stackedObject.childInfos.size() > 0) try { childInfo = (ChildInfo) stackedObject.childInfos.get(tag); } catch (final Exception e) { LOGGER.error("Error in getting child info" + e.getMessage()); } if (null == childInfo) {// no info. Probably this tag is encountered for // the first time childInfo = new ChildInfo(tag); childInfo.addInfo(parentObject); stackedObject.childInfos.put(tag, childInfo); } // is there a key to this node? name=key or name=id or name=name are // considered to be keys String key = null; try { key = atts.getValue("id"); } catch (final Exception e) { LOGGER.error("Exp=" + e.getMessage()); } if (null == key) try { key = atts.getValue("key"); } catch (final Exception e) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Exp=" + e.getMessage()); } if (null == key) try { key = atts.getValue("name"); } catch (final Exception e) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Exp=" + e.getMessage()); } return childInfo.createChild(parentObject, key); } /* * Error handler is defined and supplied to the parser to report errors encountered during parsing Note that we output the * error message, and continue.. */ private class MyErrorHandler implements ErrorHandler { /** Error handler output goes here */ private final PrintStream out; MyErrorHandler(final PrintStream out) { this.out = out; } /** * Returns a string describing parse exception details */ private String getParseExceptionInfo(final SAXParseException spe) { String systemId = spe.getSystemId(); if (systemId == null) systemId = "null"; final String info = "URI=" + systemId + " Line=" + spe.getLineNumber() + ": " + spe.getMessage(); return info; } // The following methods are standard SAX ErrorHandler methods. // See SAX documentation for more info. @Override public void warning(final SAXParseException spe) throws SAXException { out.println("Warning: " + getParseExceptionInfo(spe)); } @Override public void error(final SAXParseException spe) throws SAXException { final String message = "Error: " + getParseExceptionInfo(spe); throw new SAXException(message); } @Override public void fatalError(final SAXParseException spe) throws SAXException { final String message = "Fatal Error: " + getParseExceptionInfo(spe); throw new SAXException(message); } } /* * Holds all information required to instantiate an object whenever a tag of this type is encountered. Has method to create * child, as well as extract and store information */ private class ChildInfo { String tag; int storageType = 0; // 0=invalid, 1=newChild, 2=singleChild into a // field, 3= array 4=collection Field field = null; Class fieldType = null; Object collectionObject = null; // collected children as arraylist to be // converted to array Method addMethod = null; // execute .add() to add the child boolean addRequiresKey = false; // whether add is add(object) or // add(tag, object) Method newChildMethod = null; Method endChildMethod = null; ChildInfo(final String tag) { this.tag = tag; } /* * Extracts information about the way the child Object is to be created. Uses reflection to check what the parentObject * has planned to create add child nodes This method is long, as we explore different options.. */ public void addInfo(final Object parentObject) { // class arrays required for getMethod() calls final Class[] stringClass = { String.class }; final Class[] stringClass2 = { String.class, String.class }; final Class[] objClass = { Object.class }; final Class[] objClass2 = { Object.class, Object.class }; /* * Is this an advanced programmer who has written her own addChild(tag, key) method? */ try { newChildMethod = parentObject.getClass().getMethod( "newChild", stringClass2); // thrown out if the above line does not succeed // now that we got the method, let us set other properties of // childInfo collectionObject = parentObject; addRequiresKey = true; storageType = 1; /* * check if she has defined the optional method endChild(tag) */ try { endChildMethod = parentObject.getClass().getMethod( "endChild", stringClass); } catch (final Exception e) { LOGGER.error("Exp=" + e.getMessage()); } return; // if this method is available, we do not look for // anything else. } catch (final Exception e) { LOGGER.error("Exp=" + e.getMessage()); } /* * Is there a field with name = tag? */ try { field = ObjectGetSetter.getField(parentObject, tag);// throws // exception // if // field // not // found field.setAccessible(true); fieldType = field.getType(); storageType = 2; // object class. If it is to be a single // child // this will be over-ridden subsequently if collection is // discovered.. // no return. We should explore further.... } catch (final Exception e) { LOGGER.error("Exp=" + e.getMessage()); } /* * Is there a collection object that holds all children? Common ones are Array and HashMap. name is assumed to be * plural of tag. example, if tag=lion, we look for a field with name lions Bare with me: we do not understand English * Grammer to make child to children. We will look for 'childs' if tag is 'child' */ try { final Field f = ObjectGetSetter .getField(parentObject, tag + 's'); // thrown out // if field // not found final Class c = f.getType(); // is it an array? if (c.isArray()) { storageType = 3; // array collectionObject = new ArrayList(); // store here and // create array // later addRequiresKey = false; addMethod = ArrayList.class.getMethod("add", objClass); field = f; fieldType = c.getComponentType(); } else { Object o = f.get(parentObject); if (null == o) { o = c.newInstance(); f.setAccessible(true); f.set(parentObject, o); } try { // It could be ArrayList or HashMap. Instead of // restricting to these classes, // we allow any class so long it has an add(object) // method or put(tag, object) method addMethod = c.getMethod("add", objClass); collectionObject = o; // o stores all children addRequiresKey = false; storageType = 4; } catch (final Exception e) { try {// may be it has put(key, Object) method.. addMethod = c.getMethod("put", objClass2); collectionObject = o; addRequiresKey = true; storageType = 4; } catch (final Exception e1) { LOGGER.error("Exp=" + e1.getMessage()); } LOGGER.error("Exp ObjectGetSetter=" + e.getMessage()); } } } catch (final Exception e) { LOGGER.error("Exp=" + e.getMessage()); } // if all exploration failed, storageType will continue to be 0 // implying inability to create child object } /* * Instantiate a child object based on this info for the supplied parentObject */ public Object createChild(final Object parentObject, final String key) { Object childObject = null; final Object[] arr1 = { null }; // for method.invoke().. final Object[] arr2 = { null, null }; switch (storageType) { case 1: // newChild(tag) method try { arr2[0] = tag; arr2[1] = key; childObject = newChildMethod .invoke(parentObject, arr2); } catch (final Exception e) { LOGGER.error("Exp=" + e.getMessage()); } break; case 2: // field - single child . get it, and instantiate it if // required try { childObject = field.get(parentObject); if (childObject == null) { childObject = fieldType.newInstance(); field.set(parentObject, childObject); } } catch (final Exception e) { LOGGER.error("Exp=" + e.getMessage()); } break; case 3: // array. But a temporary ArrayList is created, and hence // adding is same as 4 case 4: try { // instantaite an object and add it to the collection childObject = fieldType.newInstance(); if (addRequiresKey) { arr2[0] = key; arr2[1] = childObject; // arr2[0] is already set to tag addMethod.invoke(collectionObject, arr2); } else { arr1[0] = childObject; addMethod.invoke(collectionObject, arr1); } } catch (final Exception e) { LOGGER.error("Exp=" + e.getMessage()); } break; default: break; } return childObject; } /* * end tag recd for the tag. do we have to do anything? */ void endChild(final Object parentObject) { switch (storageType) { case 1: // see if there is endChild() method if (endChildMethod != null) try { final Object[] a = { tag }; endChildMethod.invoke(parentObject, a); } catch (final Exception e) { LOGGER.error("Exp in endChild=" + e.getMessage()); } break; case 3: // array. convert ArrayList to array try { final ArrayList al = (ArrayList) collectionObject; final int len = al.size(); final int[] a = { len }; final Object o = Array.newInstance(fieldType, a); for (int i = 0; i < len; i++) Array.set(o, i, al.get(i)); field.setAccessible(true); field.set(parentObject, o); } catch (final Exception e) { LOGGER.error("Exp in end Child=" + e.getMessage()); } break; default: break; } } } private class StackedObject { Object object; HashMap childInfos; StackedObject(final Object object) { this.object = object; childInfos = null; } } }