package org.bifrost.xmlio.impl;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bifrost.util.text.StringHelper;
import org.bifrost.xmlio.XmlException;
import org.bifrost.xmlio.config.NamespaceMap;
import org.bifrost.xmlio.config.ObjectMap;
import org.bifrost.xmlio.config.PropertyMap;
import org.bifrost.xmlio.config.XmlIOConfig;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* <p>Class used to parse the xml into objects.</p>
* <p>
* Created: Feb 23, 2003<br/>
* Copyright: Copyright (c) 2003<br/>
* Assumptions: none<br/>
* Requires: nothing<br/>
* Required by: XmlReader<br/>
* Revision History:<br/>
* 2003-04-21 Changed to use JAXP instead of Xerces.<br/>
* </p>
* @author Donald Kittle <donald@bifrost.org>
* @version 1.0
*/
public class ReaderContentHandler extends DefaultHandler {
private Logger logger = Logger.getLogger(this.getClass().getName());
public static final String EX_SETTER_NAME =
"Cannot find appropriate setter name";
public static final String EX_SETTER =
"Cannot find appropriate setter";
public static final String EX_CREATE_OBJECT =
"Could not create object ";
public static final String EX_CORRECT_SETTER =
"Unable to find correct setter: ";
public static final String EX_WRONG_NUM_PARAMS =
"Wrong number of parameters in method.";
public static final String EX_STACK_NOT_EMPTY =
"Parser stack not empty at the end of the document.";
/**
* A stack of element and attribute names. New names get added as the xml
* is parsed. As attributes are set or elements are finshed with
* (endElement()), the names are removed from the stack.
*/
private Stack configStack;
/**
* A map of objects currently instantiated. Setting attributes will look up
* instantiated objects in this map.
*/
private Map configObjects;
/**
* The root object described in the XML.
*/
private Object root;
/**
* A flag indicating that an element has been processed already or not.
* This is used when an endElement() is called to make sure that a setter
* for the current element has been called. If startElement() or
* characters() has not been called, call a setter that takes no
* parameters.
*/
private Map elementProcessed = new HashMap();
/**
* A flag indicating that an element is an object, not an attribute.
*/
private Stack isObject = new Stack();
/**
* The packages that the objects described in the XML are defined in.
*/
private List targetPackage;
private String currentPackage;
private String lastCharacters = "";
/**
* An instance of the XmlIOConfig object
*/
XmlIOConfig config = XmlIOConfig.getInstance();
/**
* An instance of the XmlIOConfig object
*/
PropertyHelper propertyHelper = PropertyHelper.getInstance();
private Locator locator = null;
/**
* Constructor.
* @param targetPackage The package where the classes for describing the xml
* are located.
*/
public ReaderContentHandler(String thePackage)
{
targetPackage = new LinkedList();
if (thePackage == null)
thePackage = "";
StringTokenizer st = new StringTokenizer(thePackage, ":");
while(st.hasMoreElements())
{
String path = (String)st.nextElement();
path = correctPackageEnding(path);
targetPackage.add(path);
}
if (logger.isLoggable(Level.FINE))
logger.fine("Creating XML parser");
}
private String correctPackageEnding(String thePackage)
{
if (thePackage == null || "".equals(thePackage))
return thePackage;
if (!thePackage.endsWith("."))
{
StringBuffer test = new StringBuffer(thePackage);
test.append(".");
thePackage = test.toString();
test = null;
}
return thePackage;
}
/**
* Process characters - presumably this is to set an attribute. Call the
* setter on the top objects in the stack.
*/
public void characters(char[] ch, int start, int length)
throws SAXException
{
if (attributeOnStack() == false)
return;
String attributeName = (String)configStack.peek();
attributeName = getJavaName(attributeName);
Boolean called = (Boolean)elementProcessed.get(attributeName);
if (called != null && called == Boolean.TRUE)
return;
String value = (new String(ch, start, length));
StringBuffer temp = new StringBuffer();
if (lastCharacters != null)
temp.append(lastCharacters);
temp.append(value);
lastCharacters = temp.toString();
logger.fine("Processing characters: '" + value + "'");
} // end characters()
/**
* Given the xml name of an element or attribute, translate into it's java
* object or property name, as appropriate.
* @param xmlName the name of the xml tag or attribute
* @return String the java name corresponding to the xml name, or the xml
* name that was passed to the method if there was no mapping.
*/
private String getJavaName(String xmlName)
{
if (xmlName == null)
return null;
String result = null;
String className = null;
// Pull previous names from the stack
if (configStack.size() > 0)
className = (String)configStack.pop();
// Restore stack
if (className != null)
configStack.push(className);
// Check global property mappings
if (config.getPropertyMapByAlias(xmlName) != null)
result = config.getPropertyMapByAlias(xmlName).getPropertyName();
if (result == null && className != null)
{
// See if this object name is mapped
ObjectMap oMap = config.getObjectMapByName(className);
if (oMap == null)
{
className = StringHelper.capitalizeFirst(className);
oMap = config.getObjectMapByName(className);
}
if (oMap != null && oMap.getPropertyMapFromAlias(xmlName) != null)
result = oMap.getPropertyMapFromAlias(xmlName).getPropertyName();
}
if (result == null)
result = xmlName;
return result;
} // end getJavaName()
private boolean attributeOnStack()
{
String attributeName = null;
if (configStack.size() > 0)
attributeName = (String)configStack.peek();
// If the first letter of the attribute name is upper case then we are
// currently processing an object
if (Character.isUpperCase(attributeName.charAt(0)))
return false;
return true;
}
/**
* Sets the 'current' attribute to the specified value. The top object on
* the stack is the attribute to be set and the second last object on the
* stack is the object. An exception will not be thrown if there are too
* few objects on the stack as it simply means we are processing some
* whitespace or something similar.
* @param value the value to be set
* @throws SAXException if there is a problem
*/
private void setCurrentAttribute(Object value) throws SAXException
{
String attributeName = null;
String objectName = null;
if (configStack.size() > 0)
attributeName = (String)configStack.pop();
if (configStack.size() > 0)
objectName = (String)configStack.pop();
if (attributeName == null)
return;
if (objectName == null)
{
// Put attribute name back on stack
configStack.push(attributeName);
return;
}
// Push both object name and attribute name back onto stack
configStack.push(objectName);
configStack.push(attributeName);
if (attributeName.equals("") || objectName.equals(""))
return;
// Get an instance of the object to set the attribute in
Object object = getObject(objectName);
if (object == null)
{
object = createObject(objectName);
if (object == null)
throw new SAXException(EX_CREATE_OBJECT + objectName + getLocation());
}
String methodName = attributeName;
ObjectMap omap = config.getObjectMapByName(objectName);
if (omap == null)
omap = config.getObjectMapByName(nameWithoutPackage(objectName));
if (omap == null)
{
omap = ObjectMap.createFromClass(object.getClass());
config.addObjectMap(omap);
}
Method method = null;
boolean global = false;
PropertyMap pmap = omap.getPropertyMapFromName(methodName);
if (pmap == null)
pmap = omap.getPropertyMapFromName(lowerCaseFirst(methodName));
if (pmap == null)
pmap = omap.getPropertyMapFromName(capitalizeFirst(methodName));
// Check to see if it's globally defined
if (pmap == null)
{
pmap = config.getPropertyMapByName(methodName);
if (pmap == null)
pmap = config.getPropertyMapByName(lowerCaseFirst(methodName));
if (pmap == null)
pmap = config.getPropertyMapByName(capitalizeFirst(methodName));
if (pmap != null)
global = true;
}
StringBuffer trace = new StringBuffer(methodName);
if (pmap == null)
{
Object thisObject = instantiateObject(methodName);
Class superClass = null;
if (thisObject != null)
superClass = thisObject.getClass().getSuperclass();
while (superClass != null && pmap == null)
{
methodName = nameWithoutPackage(superClass.getName());
trace.append(" or ");
trace.append(methodName);
pmap = omap.getPropertyMapFromName(methodName);
if (pmap == null)
pmap = omap.getPropertyMapFromName(lowerCaseFirst(methodName));
// Check to see if it's globally defined
if (pmap == null)
{
pmap = config.getPropertyMapByName(methodName);
if (pmap == null)
pmap = config.getPropertyMapByName(capitalizeFirst(methodName));
if (pmap != null)
global = true;
}
superClass = superClass.getSuperclass();
}
}
if (pmap == null)
{
String error = EX_SETTER_NAME +" for " + trace.toString() + " in " + objectName;
logger.fine(error);
throw new SAXException(error);
}
if (pmap != null)
method = pmap.getSetter();
// If the setter method is undefined for this property map, then the
// property map was probably added to config programmatically or through
// a config file, rather than being generated from a class.
if (method == null && pmap != null)
{
// Guess what the setter might be
method = pmap.guessSetter(object);
// And cache the method if it's not a global property map
if (method != null && global == false)
pmap.setSetter(method);
}
try
{
propertyHelper.setAttribute(pmap, object, method, value);
} catch (XmlException e)
{
throw new SAXException(e.toString() + getLocation());
}
} // end setCurrentAttribute()
private String lowerCaseFirst(String source)
{
if (source == null)
return null;
if (source.equals(""))
return "";
StringBuffer result = new StringBuffer();
result.append(Character.toLowerCase(source.charAt(0)));
if (source.length() > 1)
result.append(source.substring(1));
return result.toString();
}
private String capitalizeFirst(String source)
{
if (source == null)
return null;
if (source.equals(""))
return "";
StringBuffer result = new StringBuffer();
result.append(Character.toUpperCase(source.charAt(0)));
if (source.length() > 1)
result.append(source.substring(1));
return result.toString();
}
/**
* Create an instance of the named object, trying all paths defined in the
* context.
* @param objectName the fully qualified name of the object to create
*/
private Object instantiateObject(String objectName)
{
if (objectName == null)
return null;
// Try creating an instance of the object without any changes to the name.
Object object = config.getObjectFactory().getInstance(objectName);
if (object != null)
return object;
// Try adding the various context paths to the object name
for (Iterator i = targetPackage.iterator(); i.hasNext(); )
{
currentPackage = (String)i.next();
String className = currentPackage + objectName;
object = config.getObjectFactory().getInstance(className);
if (object != null)
return object;
}
return null;
} // end instantiateObject()
/**
* Create an instance of the named object and put a reference to that object
* in the map.
* @param objectName the fully qualified name of the object to create
*/
private Object createObject(String objectName)
{
Object object = instantiateObject(objectName);
if (object != null)
{
putObject(objectName, object);
return object;
}
// Finally, see if there is a mapping from the name found in the xml to
// a different java name
return object;
} // end createObject()
private void putObject(String objectName, Object object)
{
LinkedList coll = (LinkedList)configObjects.get(objectName);
if (coll == null)
coll = new LinkedList();
coll.add(object);
configObjects.put(objectName, coll);
} // end putObject()
private Object getObject(String objectName)
{
Object result = null;
LinkedList coll = (LinkedList)configObjects.get(objectName);
if (coll != null && coll.size() > 0)
result = coll.getLast();
return result;
} // getObject()
private void removeObject(String objectName)
{
LinkedList coll = (LinkedList)configObjects.get(objectName);
if (coll != null)
{
Object last = coll.getLast();
coll.remove(last);
}
} // getObject()
/**
* Implementation of abstract method from ContentHandler.
*/
public void endDocument() throws SAXException {
if (!configStack.empty())
throw new SAXException(EX_STACK_NOT_EMPTY + getLocation() + getRemaining());
} // end endDocument()
public String getRemaining()
{
StringBuffer result = new StringBuffer();
String sep = "";
while (!configStack.isEmpty())
{
String item = (String)configStack.pop();
result.append(sep);
result.append(item);
sep = ",";
}
return result.toString();
} // end getRemaining()
/**
* Process end element. If first character in the name of the element is
* lower case, the element is an attribute so simply remove it from the
* stack. If the first character in the name of the element is upper case,
* it is an object so try to treat this current object as an attribute of
* a parent object (if a parent object exists). For instance, if the
* current element name is Value and the previous element name was AnObject,
* we would try to call a AnObject.setValue() method.
*/
public void endElement(String namespaceURI, String localName, String qName)
throws SAXException
{
if (logger.isLoggable(Level.FINE))
logger.fine("Processing end tag: " + qName);
qName = StringHelper.dashesToCapitals(qName);
String name = (String)configStack.pop();
if (name == null || "".equals(name))
return;
// Check to see if the current element is 'null' and the previous
// element is an attribute. If both are true, simply return.
if (name.equals("null"))
{
if (attributeOnStack() == true)
{
// Get the name of the property off of the stack (it was the previous
// tag), set the property to null and mark it as having been
// processed
String attributeName = (String)configStack.peek();
setCurrentAttribute(null);
elementProcessed.put(attributeName, Boolean.TRUE);
logger.fine("Null found and previous element is an attribute.");
return;
}
}
// We are not dealing with a null, push the name back onto stack and
// process it normally
if (name != null)
{
name = getJavaName(name);
configStack.push(name);
}
// If first letter of element is lower case, it's an attribute
// if (attributeOnStack() == true)
Boolean objectFlag = (Boolean)isObject.pop();
if (objectFlag == Boolean.FALSE)
{
logger.fine("End element is an attribute");
// Check to make sure that characters() was called on this element,
// otherwise check for and call a setAttribute() method with the
// lastCharacters variable as the parameter
Boolean called = (Boolean)elementProcessed.get(name);
if (called == null || called == Boolean.FALSE)
setCurrentAttribute(lastCharacters);
configStack.pop();
elementProcessed.remove(name);
return;
}
// The element is an object...
Object object = getObject(name);
// Create objects if it had not sub-elements or attributes.
if (object == null)
object = createObject(name);
if (object == null)
return;
// And remove that instance of the object out of the object map (we are
// done with it)
removeObject(name);
// Try to call a parent object's setter for this object as an attribute
setCurrentAttribute(object);
// Pull object name back off of stack (we are done with it)
configStack.pop();
} // end endElement()
/**
* Clears the root element and creates a new processing stack and object map
* for caching objects.
*/
public void startDocument() throws SAXException {
configStack = new Stack();
configObjects = new HashMap();
root = null;
} // end startDocument()
/**
* Start element implementation which simply pushes the element name onto the
* stack of elements and attributes to deal with. If the root object has not
* been created, create it using the name of this element. If this element
* has attributes, process them as if they were sub elements of this element.
*/
public void startElement(String namespaceUri, String localName, String qName,
Attributes atttributes) throws SAXException
{
// Convert the xml name to a java name
qName = StringHelper.dashesToCapitals(qName);
qName = getJavaName(qName);
String name = "";
if (configStack.size() > 0)
name = StringHelper.capitalizeFirst((String)configStack.peek());
lastCharacters = "";
String element = null;
if (configStack.size() > 0)
element = (String)configStack.peek();
if (element != null)
elementProcessed.put(element, Boolean.FALSE);
if (logger.isLoggable(Level.FINE))
logger.fine("Processing start tag: " + qName);
if (config.getObjectMapByAlias(qName) != null)
qName = config.getObjectMapByAlias(qName).getName();
// qName = StringHelper.capitalizeFirst(qName);
// Expand namespace if the element is an object (first letter is uppercase)
if (namespaceUri != null && !namespaceUri.equals("") && qName != null &&
!"".equals(qName))
{
if (Character.isUpperCase(qName.charAt(0)) == true)
{
NamespaceMap map =
(NamespaceMap)config.getNamespaceMapByUri(namespaceUri);
String packageName = null;
if (map != null)
packageName = (String)map.getPackageName();
if (packageName != null)
{
StringBuffer temp = new StringBuffer(packageName);
if (!packageName.endsWith("."))
temp.append(".");
temp.append(qName);
qName = temp.toString();
}
else
logger.info("Found no mappings for " + namespaceUri);
}
} // end namespace processing
Object obj = createObject(qName);
if (obj != null)
isObject.push(Boolean.TRUE);
else
isObject.push(Boolean.FALSE);
if (root == null)
root = obj;
if (obj != null && config.getObjectMapByName(qName) == null)
config.addClassAsMappedObject(obj.getClass());
configStack.push(qName);
// If there are no attributes, we are done.
if (atttributes == null || atttributes.getLength() < 1)
return;
// Process attributes as if they were elements.
for (int i = 0; i < atttributes.getLength(); i++)
{
startElement(namespaceUri, atttributes.getLocalName(i), atttributes.getQName(i), null);
characters(atttributes.getValue(i).toCharArray(), 0, atttributes.getValue(i).length());
endElement(namespaceUri, atttributes.getLocalName(i), atttributes.getQName(i));
}
} // end startElement()
/**
* Returns the result of the parsing (the collection of objects).
* @return Object the root object that describes the xml
*/
public Object getResult(){
return root;
} // end getResult()
/**
* Take a fully qualified name of a class and return the class name without
* any package information.
* @param source the fully qualified name of the class
* @return the name of the class without any package information
*/
private String nameWithoutPackage(String source)
{
int index = source.lastIndexOf(".");
if (index < 0 || index == source.length())
return source;
return source.substring(index + 1);
} // end nameWithoutPackage()
private String getLocation()
{
if (locator == null)
return "";
StringBuffer result = new StringBuffer();
result.append(" - line = ");
result.append(locator.getLineNumber());
result.append(", column = ");
result.append(locator.getColumnNumber());
return result.toString();
}
public void setDocumentLocator(Locator locator)
{
if (locator == null)
return;
this.locator = locator;
}
} // end ReaderContentHandler Class