<?xml version="1.0"?> <schema targetNamespace="http://www.enhydra.org" xmlns="http://www.w3.org/1999/xmlSchema" xmlns:enhydra="http://www.enhydra.org" > <complexType name="ServiceConfiguration"> <attribute name="name" type="string" /> <attribute name="version" type="float" /> </complexType> <element name="serviceConfiguration" type="ServiceConfiguration" /> <complexType name="WebServiceConfiguration" baseType="ServiceConfiguration" derivedBy="extension"> <element name="port"> <complexType> <attribute name="protocol" type="string" /> <attribute name="number" type="integer" /> <attribute name="protected" type="string" /> </complexType> </element> <element name="document"> <complexType> <attribute name="root" type="string" /> <attribute name="index" type="string" /> <attribute name="error" type="string" /> </complexType> </element> </complexType> <element name="webServiceConfiguration" type="WebServiceConfiguration" /> </schema> |
生成代码
开始生成 Java 代码之前,首先必须确定核心类的名称。将使用 org.enhydra.xml.binding 包中的 SchemaMapper,它是 Enhydra 应用服务器实用程序类集合的一部分。还可以将任何必需的支持类放到这个包中。
除了类名称以外,还必须确定用来读取和创建 XML 的 Java API。如上一篇文章中所讨论过的,三种主要选择是 SAX、DOM 和 JDOM。由于 SAX 仅仅适用于读取 XML 文档,因此它不适合创建 XML。由于在打包阶段中要将 Java 对象转换为 XML 表示,因此在此阶段中需要创建 XML。这就将选择的范围缩小到 DOM 和 JDOM。在这两种选择都可用的情况下,本例中我选择使用 JDOM API,仅为了显示其功能性(并不仅仅因为我是它的合著者之一!)。
最后,必须指出如何将 XML schema 提供给 SchemaMapper 类。通常,可以假设类的生成是脱机完成的(通过静态 main 方法)。仅通过使 main 方法调用非静态方法,还可以从运行时环境中使用类。做了这些决定后,就可以开始勾画类的框架了。
组装 SchemaMapper 类框架
要做的第一件事就是为要生成的代码设置一些基本存储器。必须能够从每个执行映射的 XML schema 生成多个接口和实现。Java HashMap 正好满足要求。键是接口或实现名称以及映射表中的值,该值是将要输出到新 Java 程序文件的实际代码。还需要存储每对接口/实现的属性(属性是在这两种类之间共享的)。这里,我再次使用 HashMap。其中,键是接口名称。但是,由于每个接口可能有多个属性,因此该值是另一个具有属性及其类型的 HashMap。最后,必须存储 XML schema 的名称空间,因为 JDOM 将使用这个名称空间来访问 XML schema 中的结构。所有这些具体信息都足以初步勾画出新类的框架,新类在清单 2 中。
还请注意在清单 2 中已添加了两个需要使用的基本方法:其中一个方法需要使用 XML schema 的 URL 来执行生成(允许它在网络可访问 schema 以及本地 schema 下运行),另一个方法将类输出到指定的目录中。最后,简单的 main 方法将 XML schema 看作一个变量,然后执行生成。
清单 2. SchemaMapper 类的框架 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.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.jdom.NoSuchAttributeException; import org.jdom.NoSuchChildException; 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 { // Perform generation } /** * <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 { // Perform output to files } /** * <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(); } } } |
In 清单 2 中,可以看到对于每个作为自变量传递的 XML schema,main 方法都调用生成过程。首先,方法会生成类。将文件名转换为 URL,并传递到 generateClasses(URL schemaURL)。然后,通过 writeClasses(File dir) 方法将类写到当前目录中(转换成 Java File: new File("."))。
任何其它 Java 类都可以在运行时进行相同的调用,并生成类。例如,一个定制类装入器也许能发现需要打包,确定仍要生成的接口和实现,并使用 SchemaMapper 类来执行该任务。所有这一切都在运行时完成。因为 generateClasses() 方法需要一个 URL,所以在网络上使用这个类非常简单。例如,可以使用它来请求从 HTTP 上公开可用的 XML schema 生成类。
由于对如何使用类做了尽量少的假设,因此它是一个普通类;程序可以同时在本地和远程使用它。并且这个类可以当作一组 Java 语言和 XML 实用程序类的一部分,而不是必须以某种特殊形式使用的专用类。这种可重用性原则对 XML 特别关键,因为在不同系统上进行网络访问和通信是 XML 的基本前提。
生成类
构建好类的框架后,就可以添加类的主体了。
我已经提到过生成过程具有递归性质。请记住这一点,需要填充 generateClasses() 方法才能开始。可以使用 JDOM 读取 XML schema,然后从 schema 中抽取每个 complexType 元素。对于这些元素中的每一个,如清单 3 所示,递归进程从 handleComplexType() 调用处开始(以后将进一步讨论)。
清单 3. The generateClasses() 方法
public void generateClasses(URL schemaURL) throws IOException { /** * Create builder to generate JDOM representation of XML Schema, * without validation and using Apache Xerces. */ 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); } } catch (JDOMException e) { throw new IOException(e.getMessage()); } } |
为简便起见,将强调一些重点,而不是详细阐述将 schema 转换为 Java 类的整个过程。可以联机查看完整的 SchemaMapper 类,或者可以下载它。
生成器必须确定在 XML schema 中找到的每个 complexType 元素是显式的(具有“类型”属性),还是隐式的(没有“类型”属性)。如果类型是显式的,则类型将成为接口名称,并且首字母大写。如果类型是隐式的,那么将根据特性名称构造接口名称。清单 4 中显示了处理这个逻辑的代码段。(如要了解更多数据绑定的定义,请参阅侧栏,术语解释。)
清单 4. 确定接口名称 // Determine if this is an explict or implicit type
String type = null; // Handle extension, if needed String baseType = null; try { // Assume that we are dealing with an explicit type type = complexType.getAttribute("name") .getValue(); } catch (NoSuchAttributeException e) { /* * 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. */ try { type = new StringBuffer() .append(BindingUtils.initialCaps( complexType.getParent() .getAttribute("name") .getValue())) .append("Type") .toString(); } catch (NoSuchAttributeException nsae) { // Shouldn't happen in schema-valid documents throw new IOException("All elements must at have a name."); } } |
因此,根据代码中的命名约定,具有 ServiceConfiguration 类型的元素将生成名为 ServiceConfiguration 的 Java 接口。名为 port 但没有显式类型的元素将生成叫做 PortType 的 Java 接口。它采用元素名称 (port),将首字母转成大写 (Port),再加上单词 Type,就得到了 PortType。
同样,所有实现类都使用接口名称,然后添加缩写 Impl。所以,最终实现类是 ServiceConfigurationImpl 和 PortTypeImpl。
使用这些命名约定,您可以很容易地确定将数据约束映射到 Java 接口会得到哪些 Java 类。如果设置了应用程序在运行时装入类,那么类装入器或其它实用程序可以迅速确定是否已装入了所需的类。类装入器或实用程序只要从 XML schema 中找出生成的类名称,然后尝试装入它们就可以了。命名逻辑是事先确定的,因此检查起来非常方便。
一旦确定了名称,就可以生成接口和实现类的框架(请参阅清单 5)。
清单 5. 生成代码
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"); // Add in properties and methods // Close up interface and implementation classes interfaceCode.append("}"); implementationCode.append("}"); |
实际上,生成属性和方法是相当简单的。将接口和相应实现的名称添加到类的存储器中,然后是右花括号,它们的作用是结束类。像这样成对生成类,而不是单独生成类,将使同时在接口和实现反映出该过程变得简单。检查源代码(请参阅参考资料),就可以得到足够的解释。
清单 5 中的粗体注释表示源列表中的多行代码。在这里精简代码是为了保持简洁。对于正在创建的 XML schema 的每个特性(由 schema attribute 表示),都会将读方法和写方法添加到接口和实现(实现还有执行方法逻辑的代码)。同时,将为实现类的代码添加变量。
最终结果就是本系列第一部分中生成的类。可以在这里查看它们,或者与本文中的其余代码一起下载(请参阅参考资料):
ServiceConfiguration.java
ServiceConfigurationImpl.java
PortType.java
PortTypeImpl.java
DocumentType.java
DocumentTypeImpl.java
WebServiceConfiguration.java
WebServiceConfigurationImpl.java
有两个辅助程序类也将参与类生成:
BindingUtils,将首字母变成大写。虽然,可以将这个方法添加到生成器类,但我打算以后在打包和解包类时再使用该方法,所以我将它归到这个辅助程序类中。可以联机查看 BindingUtils,或者可以下载它。
DataMapping,SchemaMapper 类用来转换数据类型。可以联机查看源码或者下载源码。
完成包
如许多其它开放源码软件,在这里显示的数据绑定包是一项正在进行中的工作。虽然它已经初具规模,但仍有很大空间可用于添加更多功能和做改进。因此,以这段代码为基础,可以有许多方式应用程序中加以衍生。
可以重新使用该样本代码,以将 XML schema 的数据约束转换为类型安全的 Java 接口和实现。例如,迄今为止,示例代码还没有处理 XML schema 中可能指定的范围。而对于许多 XML 开发人员,那些数据范围才是使用 schema 的真正原因。然后,请考虑清单 6 中 Web 服务的扩充 XML schema。
清单 6. 带扩充约束的 Web 服务配置
<?xml version="1.0"?> <schema targetNamespace="http://www.enhydra.org" xmlns="http://www.w3.org/1999/xmlSchema" xmlns:enhydra="http://www.enhydra.org" > <complexType name="ServiceConfiguration"> <attribute name="name" type="string" /> <attribute name="version" type="float" /> </complexType> <element name="serviceConfiguration" type="ServiceConfiguration" /> <complexType name="WebServiceConfiguration" baseType="ServiceConfiguration" derivedBy="extension"> <element name="port"> <complexType> <attribute name="protocol" type="string" /> <attribute name="number"> <simpleType base="integer"> <minExclusive value="0" /> <maxInclusive value="32767" /> </simpleType> </attribute> <attribute name="protected" type="string" /> </complexType> </element> <element name="document"> <complexType> <attribute name="root" type="string" /> <attribute name="index" type="string" /> <attribute name="error" type="string" /> </complexType> </element> </complexType> <element name="webServiceConfiguration" type="WebServiceConfiguration" /> </schema> |
public class PortTypeImpl implements PortType { private String protocol; private int number; private String protected; public void setNumber(int number) { if ((number > 0) && (number <= 32767)) { this.number = number; } else { throw IllegalArgumentException("Argument must be greater than 0 and less than or equal to 32767"); } } public int getNumber() { return number; } public void setProtocol(String protocol) { this.protocol = protocol; } public String getProtocol() { return protocol; } public void setProtected(String protected) { this.protected = protected; } public String getProtected() { return protected; } } |