/*
* Enhydra Java Application Server Project
*
* The contents of this file are subject to the Enhydra Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License on
* the Enhydra web site ( http://www.enhydra.org/ ).
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific terms governing rights and limitations
* under the License.
*
* The Initial Developer of the Enhydra Application Server is Lutris
* Technologies, Inc. The Enhydra Application Server and portions created
* by Lutris Technologies, Inc. are Copyright Lutris Technologies, Inc.
* All Rights Reserved.
*/
package org.enhydra.zeus;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
// Zeus imports
import org.enhydra.zeus.util.ClassUtils;
import org.enhydra.zeus.util.NamingUtils;
// JDOM imports
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.DocType;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
/**
* <p>
* <code>Unmarshaller</code> takes an XML instance document, which should
* conform to some set of XML constraints, and creates a Java object from
* the XML document. This object is an instance of the implementation class,
* which in turn implements the interface, created using an implementation of
* <code>{@link Generator}</code>.
* </p>
* <p>
* It also assumes that the generated interface and implementation classes
* are in the classpath, as it will perform a <code>Class.forName()</code> on
* the classes.
* </p>
*
* @author Brett McLaughlin
* @author Maciej Zawadzki
*/
public class Unmarshaller {
/** Constant used to signal whether debug messages should be printed out */
protected boolean debug = false;
/** Store interface/implementation class mappings */
private Map implClasses;
/** The package name of the interface/implementation classes */
private String javaPackage;
/**
* Prefix to be prepended to the name of every generated
* interface and class.
*/
protected String namePrefix;
/**
* <p>
* Simple constructor.
* </p>
*/
public Unmarshaller() {
implClasses = new HashMap();
namePrefix = "";
}
/**
* <p>
* For the supplied interface name, this will allow a customized
* implementation class to be set. This effectively allows the user to
* specify their own implementation class to use instead of the default,
* Zeus-generated class.
* </p>
*
* @param interfaceName <code>String</code> name of interface being
* deal with.
* @param implClassName <code>String</code> name of class to load when
* implementations of <code>interfaceName</code> are needed.
*/
public void setImplClass(String interfaceName, String implClassName) {
implClasses.put(interfaceName, implClassName);
}
/**
* <p>
* This method allows the developer to set a prefix that
* will be prepended to the name of every generated
* interface and class.
* </p>
*
* @param String prefix that will be prepended to the
* name of every generated interface and class
*/
public void setNamePrefix(String namePrefix) {
if (namePrefix != null) {
this.namePrefix = namePrefix;
}
}
/**
* <p>
* This method allows the developer to get the prefix that
* will be prepended to the name of every generated
* interface and class. Returns an empty string in the
* case that a prefix is not prepended.
* </p>
*
* @return <code>String</code> prefix that will be prepended
* to the name of every generated interface and class
*/
public String getNamePrefix() {
return namePrefix;
}
/**
* <p>
* For the supplied interface name, this will return the current
* implementation class associated with the interface. If no class has
* been specified, this will return the default, Zeus-generated class
* name for the interface.
* </p>
*
* @param interfaceName <code>String</code> name of interface being
* dealt with.
* @return <code>String</code> - implementation class for supplied
* <code>interfaceName</code>.
*/
public String getImplClass(String interfaceName) {
String implClass = (String)implClasses.get(interfaceName);
if (implClass == null) {
// Construct class name for Zeus binding class
implClass = new StringBuffer()
.append(namePrefix)
.append(interfaceName)
.append("Impl")
.toString();
}
return implClass;
}
/**
* <p>
* Returns the Java package to unmarshall classes to.
* </p>
*
* @return <code>String</code> - the Java package to unmarshall to.
*/
public String getJavaPackage() {
if (javaPackage == null) {
javaPackage = "";
}
return javaPackage;
}
/**
* <p>
* Sets the Java package to unmarshall classes to.
* </p>
*
* @param javaPackage <code>String</code> the Java package to unmarshall
* to.
*/
public void setJavaPackage(String javaPackage) {
this.javaPackage = javaPackage;
}
/**
* <p>
* This method is the public entry point for unmarshalling an object from
* an XML instance document. Note that the raw converted object is not
* directly returned, but is available through the
* <code>{@link UnmarshalledObject#getObject}</code> method on
* <code>{@link UnmarshalledObject}</code>.
* </p><p>
* XXX: We need to eventually put in Zeus exceptions here,
* not IOExceptions.
* </p>
*
* @param instanceURL <code>URL</code> for the instance document.
* @return <code>UnmarshalledObject</code> - the created Java object, or
* <code>null</code> if problems occur in a way that does not
* generate an <code>Exception</code>.
* @throws <code>IOException</code> when errors in binding occur.
*/
public UnmarshalledObject unmarshal(Source sourceXML) throws IOException {
// Obtain the JDOM Document from the supplied source
Document doc = sourceXML.getDocument();
Element rootElement = doc.getRootElement();
// Convert to Java
Object obj = getJavaRepresentation(rootElement);
// Wrap in UnmarshalledObject
DocType docType = doc.getDocType();
if (docType != null) {
return new UnmarshalledObject(obj,
docType.getPublicID(),
docType.getSystemID());
} else {
return new UnmarshalledObject(obj);
}
}
/**
* <p>
* This will take an XML document fragment, starting with the provided
* <code>Element</code>, and convert it to a Java representation.
* </p>
*
* @param rootElement <code>Element</code> to begin processing on.
* @return <code>Object</code> - unmarshalled Java object.
* @throws <code>IOException</code> - when XML cannot be converted to Java.
*/
private Object getJavaRepresentation(Element element)
throws IOException {
// If debugging, print out the element being processed
if (debug) {
System.out.println("Unmarshaller.getJavaRepresentation: " +
element.getName());
}
Object obj = null;
String className = null;
try {
Namespace ns = element.getNamespace();
// Class name is root element name, with initial caps, plus "Impl"
className = getImplClass(NamingUtils.getJavaClassName(
element.getName()));
Class objectClass;
// Get the class
String pkg = getJavaPackage();
if (pkg.equals("")) {
objectClass = getClass().getClassLoader().loadClass(className);
} else {
objectClass =
getClass().getClassLoader().loadClass(new StringBuffer()
.append(pkg)
.append(".")
.append(className)
.toString());
}
// Get a new instance of this class
obj = objectClass.newInstance();
// For each attribute, get its name and call mutator
List attributes = element.getAttributes();
Method[] methods = objectClass.getMethods();
for (Iterator i = attributes.iterator(); i.hasNext(); ) {
Attribute att = (Attribute)i.next();
// If debugging, print out the attribute being processed
if (debug) {
System.out.println(
" Processing attribute : " + att.getName());
}
// We only want attributes for this namespace
if ((!att.getNamespace().equals(ns)) &&
(!att.getNamespace().equals(Namespace.NO_NAMESPACE))) {
continue;
}
// Determine method to call - no Lists to worry about
// with attributes
String methodName = new StringBuffer()
.append("set")
.append(NamingUtils.getJavaClassName(att.getName()))
.toString();
// Find the method to call, and its parameter type
for (int j=0; j<methods.length; j++) {
if (methods[j].getName().equals(methodName)) {
// Since all mutators have one param, get the first one
Class[] paramTypes = methods[j].getParameterTypes();
Class paramType = paramTypes[0];
// Convert the type we have to the correct type
Object param =
ClassUtils.getParameter(att.getValue(), paramType);
// Invoke the method
methods[j].invoke(obj, new Object[] { param });
}
}
}
// Now do complex objects
List children = element.getChildren();
for (Iterator i = children.iterator(); i.hasNext(); ) {
Element child = (Element)i.next();
// If debugging, print out the element being processed
if (debug) {
System.out.println(" Processing Child Element: " +
child.getName());
}
// We only want elements for this namespace
if ((!child.getNamespace().equals(ns)) &&
(!child.getNamespace().equals(Namespace.NO_NAMESPACE))) {
continue;
}
// Determine method to call - start with default name
String singularMethodName = new StringBuffer()
.append("set")
.append(NamingUtils.getJavaClassName(child.getName()))
.toString();
String listMethodName = new StringBuffer()
.append("add")
.append(NamingUtils.getJavaClassName(child.getName()))
.toString();
// Find the method to call, and its parameter type
for (int j=0; j<methods.length; j++) {
if (methods[j].getName().equals(singularMethodName)) {
// Since all mutators have one param, get the first one
Class[] paramTypes = methods[j].getParameterTypes();
Class paramType = paramTypes[0];
// Convert the type we have to the correct type
Object param = null;
if (isSimpleElement(child)) {
param = child.getTextTrim();
} else {
param = getJavaRepresentation(child);
}
// Invoke the method:
// In case the arguments are wrong, the exception
// is caught and a recursive call is made
try {
methods[j].invoke(obj, new Object[] { param });
} catch (IllegalArgumentException ilex) {
param = getJavaRepresentation(child);
methods[j].invoke(obj, new Object[] { param });
}
break;
} else if (methods[j].getName().equals(listMethodName)) {
Class[] paramTypes = methods[j].getParameterTypes();
Class paramType = paramTypes[0];
// Convert the type we have to the correct type
Object param = null;
if (isSimpleElement(child)) {
param = child.getTextTrim();
} else {
param = getJavaRepresentation(child);
}
// Invoke the method:
// In case the arguments are wrong, the exception
// is caught and a recursive call is made
try {
methods[j].invoke(obj, new Object[] { param });
} catch (IllegalArgumentException ilex) {
param = getJavaRepresentation(child);
}
break;
}
}
}
// Finally, deal with element content
String content = element.getTextTrim();
if ((content != null) && (content.length() > 0)) {
String methodName = "setValue";
Method method
= objectClass.getMethod(methodName,
new Class[] { String.class });
method.invoke(obj, new Object[] { content });
}
} catch (ClassNotFoundException e) {
throw new IOException("The class that the XML instance document " +
"should be unmarshalled as an instance of, " +
className +
", must be generated and on the classpath. " +
"The current classpath is '" +
System.getProperty("java.class.path") + "'.");
} catch (InstantiationException e) {
throw new IOException("Could not create a new instance of the " +
"class " + className);
} catch (IllegalAccessException e) {
throw new IOException("Could not access the class " + className);
} catch (IllegalArgumentException e) {
throw new IOException("Could not pass arguments to mutator " +
"method on new instance.");
} catch (InvocationTargetException e) {
throw new IOException("Error in accessing mutator on new " +
"instance of class.");
} catch (NoSuchMethodException e) {
throw new IOException(
"Error in setting data method on new instance of class.");
}
return obj;
}
/**
* <p>
* This will determine if an element is "simple", having neither
* attributes nor child elements.
* </p>
*
* @param element <code>Element</code> to inspect
* @return <code>boolean</code> - whether the element is simple.
*/
private boolean isSimpleElement(Element element) {
boolean simple = true;
if (element.getChildren().isEmpty()) {
List attributes = element.getAttributes();
int size = attributes.size();
Attribute attr = null;
String typeStr = "dt_";
// FIXME: If there is an implied attribute, then the element may
// look like a simple element even though it is not.
for (int i = 0; i < size; i++) {
attr = (Attribute)attributes.get(i);
if (!attr.getName().startsWith(typeStr)) {
simple = false;
break;
}
}
} else {
simple = false;
}
return simple;
}
}
|