package org.enhydra.xml.binding;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.List;
// JDOM classes used for document representation
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
/**
* <p>
* <code>SchemaMapper</code> handles generation of Java interfaces and classes
* from an XML Schema, essentially allowing data contracts to be set up
* for the binding of XML instance documents to Java objects.
* </p>
*
* @author Brett McLaughlin
*/
public class SchemaMapper {
/** Storage for code for interfaces */
private Map interfaces;
/** Storage for code for implementations */
private Map implementations;
/** Properties that accessor/mutators should be created for */
protected Map properties;
/** XML Schema Namespace */
private Namespace schemaNamespace;
/** XML Schema Namespace URI */
private static final String SCHEMA_NAMESPACE_URI =
"http://www.w3.org/1999/XMLSchema";
/**
* <p>
* Allocate storage and set up defaults.
* </p>
*/
public SchemaMapper() {
interfaces = new HashMap();
implementations = new HashMap();
properties = new HashMap();
schemaNamespace = Namespace.getNamespace(SCHEMA_NAMESPACE_URI);
}
/**
* <p>
* This is the "entry point" for generation of Java classes from an XML
* Schema. It allows a schema to be supplied, via <code>URL</code>,
* and that schema is used for input to generation.
* </p>
*
* @param schemaURL <code>URL</code> at which XML Schema is located.
* @throws <code>IOException</code> - when problems in generation occur.
*/
public void generateClasses(URL schemaURL) throws IOException {
/**
* Create builder to generate JDOM representation of XML Schema,
* without validation and using Apache Xerces.
*/
// XXX: Allow validation, and allow alternate parsers
SAXBuilder builder = new SAXBuilder();
try {
Document schemaDoc = builder.build(schemaURL);
// Handle complex types
List complexTypes = schemaDoc.getRootElement()
.getChildren("complexType",
schemaNamespace);
for (Iterator i = complexTypes.iterator(); i.hasNext(); ) {
// Iterate and handle
Element complexType = (Element)i.next();
handleComplexType(complexType);
}
// Handle elements (at top level)
} catch (JDOMException e) {
throw new IOException(e.getMessage());
}
}
/**
* <p>
* This will write out the generated classes to the supplied stream.
* </p>
*
* @param directory <code>File</code> to write to (should be a directory).
* @throws <code>IOException</code> - when output errors occur.
*/
public void writeClasses(File dir) throws IOException {
if (!dir.isDirectory()) {
throw new IOException("A directory must be supplied for output.");
}
// First do interfaces
for (Iterator i = interfaces.keySet().iterator(); i.hasNext(); ) {
String interfaceName = (String)i.next();
outputFile(dir, interfaceName + ".java",
(String)interfaces.get(interfaceName));
}
// Next do implementation classes
for (Iterator i = implementations.keySet().iterator(); i.hasNext(); ) {
String implementationName = (String)i.next();
outputFile(dir, implementationName + ".java",
(String)implementations.get(implementationName));
}
}
/**
* <p>
* This will handle the generation/conversion of a
* <code>complexType</code> into a Java interface.
* </p>
*
* @param complexType <code>Element</code> to generate from.
* @throws <code>IOException</code> - when generation fails.
*/
private void handleComplexType(Element complexType) throws IOException {
// Determine if this is an explict or implicit type
String type = null;
// Handle extension, if needed
String baseType = null;
// Assume that we are dealing with an explicit type
Attribute typeAttribute = complexType.getAttribute("name");
if (typeAttribute != null) {
type = typeAttribute.getValue();
} else {
/*
* It is safe with an implicit type to assume that the parent
* is of type "element", has no "type" attribute, and that we
* can derive the type as the value of the element's "name"
* attribute with the word "Type" appended to it.
* It's also sage to assume that any correctly formed XML Schema
* will have a "name" attribute, so we don't need to check
* for null values here.
*/
type = new StringBuffer()
.append(BindingUtils.initialCaps(
complexType.getParent()
.getAttribute("name")
.getValue()))
.append("Type")
.toString();
}
// See if extension is occurring
Attribute baseTypeAttribute = complexType.getAttribute("baseType");
if (baseTypeAttribute != null) {
baseType = baseTypeAttribute.getValue();
// Assume extension by default
String derivedBy = "extension";
Attribute derivedByAttribute = complexType.getAttribute("derivedBy");
if (derivedByAttribute != null) {
derivedBy = derivedByAttribute.getValue();
}
// Only support extension, not restriction
if (!derivedBy.equals("extension")) {
throw new IOException(
"Derivation of types is only allowed by extension.");
}
}
// Capitalize for name of interface
String interfaceName = BindingUtils.initialCaps(type);
// Get name of implementation class
String implementationName = new StringBuffer()
.append(interfaceName)
.append("Impl")
.toString();
// If this is an explicit type, begin class generation
StringBuffer interfaceCode = new StringBuffer();
StringBuffer implementationCode = new StringBuffer();
/*
* Start writing out the interface and implementation class
* definitions.
*/
interfaceCode.append("public interface ")
.append(interfaceName);
// Add in extension if appropriate
if (baseType != null) {
interfaceCode.append(" extends ")
.append(baseType);
}
interfaceCode.append(" {\n");
implementationCode.append("public class ")
.append(implementationName);
// Add in extension if appropriate
if (baseType != null) {
implementationCode.append(" extends ")
.append(baseType)
.append("Impl");
}
implementationCode.append(" implements ")
.append(interfaceName)
.append(" {\n");
// Allocate storage for the properties of the interface/impl
properties.put(interfaceName, new HashMap());
// First, generate properties from elements and attributes
implementationCode.append(
getPropertiesCode(complexType.getChildren(), interfaceName));
// Generate accessor and mutator methods from properties
interfaceCode.append(
getInterfaceMethodsCode(interfaceName));
implementationCode.append(
getImplementationMethodsCode(interfaceName));
// Now handle elements
List elements = complexType.getChildren("element", schemaNamespace);
// Now create method declarations from elements
for (Iterator i = elements.iterator(); i.hasNext(); ) {
Element element = (Element)i.next();
//handleElement(element);
}
// Close up interface and implementation classes
interfaceCode.append("}");
implementationCode.append("}");
// Add code to overall storage for later output
interfaces.put(interfaceName,
interfaceCode.toString());
implementations.put(implementationName,
implementationCode.toString());
}
/**
* <p>
* This handles the generation of properties from a <code>List</code>
* of JDOM <code>Element</code>s. For each declared attribute, and
* for each declared element without an implicit type, a property
* is generated. For each declared element with an implicit type,
* the type is queried for the variable type, and the property
* is typed accordingly.
* </p>
*
* @param list <code>List</code> of children.
* @param interfaceName <code>String</code> name of interface being coded.
* @return <code>String</code> - content to add to calling code.
* @throws <code>IOException</code> - when XML Schema is wrong.
*/
protected String getPropertiesCode(List list, String interfaceName)
throws IOException {
StringBuffer code = new StringBuffer();
for (Iterator i = list.iterator(); i.hasNext(); ) {
Element element = (Element)i.next();
String name = element.getName();
String propertyName = null;
String propertyType = null;
Attribute propertyNameAttribute = element.getAttribute("name");
if (propertyNameAttribute != null) {
propertyName = propertyNameAttribute.getValue();
} else {
throw new IOException("All elements and attributes must be " +
"named for data binding.");
}
// If an attribute, add as a property
if (name.equals("attribute")) {
Attribute propertyTypeAttribute = element.getAttribute("type");
if (propertyTypeAttribute != null) {
propertyType =
DataMapping.getInstance()
.getJavaType(propertyTypeAttribute.getValue());
code.append(" private ")
.append(propertyType)
.append(" ")
.append(propertyName)
.append(";\n");
} else {
throw new IOException("All attributes must be typed " +
"for data binding.");
}
} else if (name.equals("element")) {
// Either get type explicitly, or from complex type
Attribute propertyTypeAttribute = element.getAttribute("type");
if (propertyTypeAttribute != null) {
// If no type attribute, this will jump to the else block
propertyType =
DataMapping.getInstance()
.getJavaType(propertyTypeAttribute.getValue());
code.append(" private ")
.append(propertyType)
.append(" ")
.append(propertyName)
.append(";\n");
} else {
/*
* Since Java names can start with an underscore,
* it isn't safe to use an uppercase version of the
* property name (port -> Port), so we append type
* to the uppercase version of the property name.
*/
propertyType = new StringBuffer()
.append(BindingUtils.initialCaps(propertyName))
.append("Type")
.toString();
code.append(" private ")
.append(propertyType)
.append(" ")
.append(propertyName)
.append(";\n");
// Generate the interface/class for this complexType
Element complexTypeElement =
element.getChild("complexType", schemaNamespace);
if (complexTypeElement != null) {
handleComplexType(complexTypeElement);
} else {
throw new IOException("All elements must either " +
"reference an existing type, or define a new one.");
}
}
}
// Let this interface know about the new property
((HashMap)properties.get(interfaceName))
.put(propertyName, propertyType);
}
return code.toString();
}
/**
* <p>
* This will generate the accessor and mutator methods for the supplied
* interface, based on the properties stored for that interface within
* the <code>{@link #getProperties}</code> method.
* </p>
*
* @param interfaceName <code>String</code> name of interface to code.
*/
protected String getInterfaceMethodsCode(String interfaceName) {
StringBuffer code = new StringBuffer();
Map propertiesMap = (Map)properties.get(interfaceName);
for (Iterator i = propertiesMap.keySet().iterator(); i.hasNext(); ) {
String propertyName = (String)i.next();
String propertyType = (String)propertiesMap.get(propertyName);
// Build mutator method
code.append(" public void set")
.append(BindingUtils.initialCaps(propertyName))
.append("(")
.append(propertyType)
.append(" ")
.append(propertyName)
.append(");\n");
// Build accessor method
code.append(" public ")
.append(propertyType)
.append(" get")
.append(BindingUtils.initialCaps(propertyName))
.append("();\n");
}
return code.toString();
}
/**
* <p>
* This will generate the accessor and mutator methods for the
* implementation of the supplied interface, based on the properties
* stored for that interface within the
* <code>{@link #getProperties}</code> method.
* </p>
*
* @param interfaceName <code>String</code> name of interface that this
* implementation implements.
*/
protected String getImplementationMethodsCode(String interfaceName) {
StringBuffer code = new StringBuffer();
Map propertiesMap = (Map)properties.get(interfaceName);
for (Iterator i = propertiesMap.keySet().iterator(); i.hasNext(); ) {
String propertyName = (String)i.next();
String propertyType = (String)propertiesMap.get(propertyName);
// Build mutator method
code.append("\n public void set")
.append(BindingUtils.initialCaps(propertyName))
.append("(")
.append(propertyType)
.append(" ")
.append(propertyName)
.append(") {\n")
.append(" this.")
.append(propertyName)
.append(" = ")
.append(propertyName)
.append(";\n")
.append(" }\n");
// Build accessor method
code.append("\n public ")
.append(propertyType)
.append(" get")
.append(BindingUtils.initialCaps(propertyName))
.append("() {\n")
.append(" return ")
.append(propertyName)
.append(";\n")
.append(" }\n");
}
return code.toString();
}
/**
* <p>
* Simple utility method to output content to a new file on the local
* file system.
* </p>
*
* @param dir <code>File</code> directory to create file in.
* @param filename <code>String</code> name for new file.
* @param content <code>String</code> content to put in file.
* @throws <code>IOException</code> - when errors occur in output.
*/
private void outputFile(File dir, String filename, String content)
throws IOException {
File file = new File(dir, filename);
FileWriter writer = new FileWriter(file);
writer.write(content);
writer.flush();
writer.close();
}
/**
* <p>
* This provides a static entry point for class generation from
* XML Schemas.
* </p>
*
* @param args <code>String[]</code> list of files to parse.
*/
public static void main(String[] args) {
SchemaMapper mapper = new SchemaMapper();
try {
for (int i=0; i<args.length; i++) {
File file = new File(args[i]);
mapper.generateClasses(file.toURL());
mapper.writeClasses(new File("."));
}
} catch (FileNotFoundException e) {
System.out.println("Could not locate XML Schema: ");
e.printStackTrace();
} catch (IOException e) {
System.out.println("Java class generation failed: ");
e.printStackTrace();
}
}
}
|