内容


将 XML 数据导入 Google App Engine

将本地存储在 XML 文件中的数据批量上传到 Google App Engine 的持久对象数据库中

Comments

背景

,Google App Engine (GAE)(参见 参考资料 中的链接)是一个免费 web 应用程序,用于托管来自 Google 的服务,于 2008 年 4 月发布。它最初只支持用 Python 开发的应用程序,2009 年 4 月添加了 Java 语言支持。

为应用程序提供的开发环境在开发过程中对持久数据创建一个本地数据库,站点本身允许将数据存储为持久对象,或实体。这些实体使用通过 Java Data Object (JDO) 注释的 Java Objects (POJOs) 创建。但是,开发环境无法在这两个数据库(本地和已部署)之间直接上传数据。Python 环境允许批量上传以 CSV 格式存储的数据。但它并不正式支持 Java 语言数据的原生批量上传。推荐方法是使用应用程序的 Python 版本并使用 Java 类访问数据,但这要求了解 Python 的工作原理,并依赖用 CSV 格式表示数据的能力。

XML 是一种灵活的、基于文本的格式。近年来,在线和离线应用程序越来越多地将数据存储为 XML,以便以多种方式使用。尽管 XML 在 Internet 上无处不在,但并没有提供一种批量上传服务来上传存储在 XML 文档中的数据。

SAX 是针对 XML 的一个序列访问解析器 API。当您为一个 XML 文档编写一个基于 SAX 的解析器时,可以使用几种回调方法,它们在解析该文档的过程中遇到各种文档元素时触发(比如文档开头、一个 XML 元素的开头、一个元素的末尾、一些字符,等等)。

简单 XML 持久性

要将来自一个 XML 文档的数据添加到 GAE 上的数据存储,最简单的方法是将该文档上传为应用程序的一部分,并使用一个基于 SAX 的自定义解析器基于文档中的每个条目在应用程序中创建一个类。

让我们以一个简单 XML 文档(包含一个组织的员工列表)为例,将它添加到一个 GAE 项目,为其中的每个元素创建一个类,然后将它们存储为实体。

XML 文档 employees.xml(参见 清单 1)拥有一个简单格式。每个员工拥有一个单一属性(id)和 4 个元素:firstName, surName, emailAddresshireDate

此 employees.xml 文件将在本文中使用。您可以从 下载 部分下载该文件和本文描述的其他源文件。

清单 1. employees.xml
<?xml version="1.0" encoding="UTF-8"?>
<employees>
    <employee id="1">
        <firstName>Rickey</firstName>
        <surName>Torres</surName>
        <emailAddress>rickey.torres@employer.com</emailAddress>
        <hireDate>1996-09-17</hireDate>
    </employee>
    <employee id="2">
        <firstName>Karisa</firstName>
        <surName>Moore</surName>
        <emailAddress>karisa.moore@employer.com</emailAddress>
       <hireDate>1996-04-08</hireDate>
    </employee>
    <employee id="3">
        <firstName>Aaron</firstName>
        <surName>Wilson</surName>
        <emailAddress>aaron.wilson@employer.com</emailAddress>
        <hireDate>2000-01-05</hireDate>
    </employee>
</employees>

清单 2 展示如何将这些元素表示为以下 POJO 类并使用 JDO 注释来注释它(这个注释类中假定适当的 Java 导入和一些 set 函数)。

清单 2. Employee.java
POJO Employee.javaAnnotated Employee.java
Public class Employee {

    private Long id;
    private String firstName;
    private String surName;
    private String emailAddress;
    private Date hireDate;

    public Employee() {

    }

    public void setFirstName(String firstName) {
       this.firstName = firstName;
    }
    public void setSurName(String surName) {
       this.surName = surName;
    }
    public void 
       setEmailAddress(String emailAddress) {
       this.emailAddress = emailAddress;
    }
    public void setHireDate(Date hireDate) {
       this.hireDate = hireDate;
    }
    public void setId(Long id) {
       this.id = id;
    }
}
@PersistenceCapable(identityType = 
                   IdentityType.APPLICATION)
public class Employee {

    @PrimaryKey
    @Persistent(valueStrategy = 
                   IdGeneratorStrategy.IDENTITY)
    private Long  id;
    @Persistent
    private String firstName;
    @Persistent
    private String surName;
    @Persistent
    private String emailAddress;
    @Persistent
    private Date hireDate;

    public Employee() {

    }
}

要用 Java 语言创建一个 SAX Parser,扩展 org.xml.sax.helpers.DefaultHandler 类并覆盖解析文档需要的方法,如 清单 3 所示。

清单 3. EmployeeHandler.java
package com.xmlimport.employee;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jdo.PersistenceManager;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.xmlimport.XMLImportPersistenceManagerFactory;

public class EmployeeHandler extends DefaultHandler {
    private static final Logger log = Logger.getLogger(EmployeeHandler.class.getName());
    private static final SimpleDateFormat hireDateFormat = 
				new SimpleDateFormat("yyyy-MM-dd");

    private Stack<Employee> employeeStack;
        private ArrayList<Employee> employees;
        private PersistenceManager pm = null;
        private String characters;
	
        public EmployeeHandler() {

                SAXParserFactory factory = SAXParserFactory.newInstance();
                try {

             pm = XMLImportPersistenceManagerFactory.get().getPersistenceManager();
             SAXParser saxParser = factory.newSAXParser();
             saxParser.parse(new InputSource("./employees.xml"), this);
             pm.makePersistentAll(employees);

                } catch (Throwable t) {

                    t.printStackTrace();

                }
                finally {

                    pm.close();
                }

	}

    public void startDocument() 
                throws SAXException {

                employeeStack = new Stack<Employee>();
                employees = new ArrayList<Employee>();
		
    }

        public void startElement(String namespaceURI, 
                             String localName,
                             String qualifiedName,
                             Attributes attributes) 
                throws SAXException {

                if (qualifiedName.equals("employee")) {

                    Employee employee = new Employee();
                    employee.setId(Long.parseLong(attributes.getValue("id")));
                    employeeStack.push(employee);

                }

        }

        public void endElement(String namespaceURI, 
                           String simpleName,
                           String qualifiedName) 
                throws SAXException {

                if (!employeeStack.isEmpty()) {

                    if (qualifiedName.equals("employee")) {

                        employees.add(employeeStack.pop());


                    } 
                    else if (qualifiedName.equals("firstName")) {

                        Employee employee = employeeStack.pop();
                        employee.setFirstName(characters);
                        employeeStack.push(employee);

                    } 
                    else if (qualifiedName.equals("surName")) {

                        Employee employee = employeeStack.pop();
                        employee.setSurName(characters);
                        employeeStack.push(employee);

                    } 
                    else if (qualifiedName.equals("emailAddress")) {

                        Employee employee = employeeStack.pop();
                        employee.setEmailAddress(characters);
                        employeeStack.push(employee);

                    } 
                    else if (qualifiedName.equals("hireDate")) {

                        Employee employee = employeeStack.pop();
                        try {
                        employee.setHireDate(hireDateFormat.parse(characters));
                        } catch (ParseException e) {

                    log.log(Level.FINE, "Could not parse date {0}", characters);
                        }
                        employeeStack.push(employee);

                    }

                }

        }

        public void characters(char buf[], int offset, int len) 
                throws SAXException {

                characters = new String(buf, offset, len);

        }

}

每当您解析一个新的 <employee> 元素时,您将创建一个新的 Employee 对象并将其添加到一个 Stack。每当其他元素被解析时,您将这个 Employee 对象弹出 Stack,调用相关的 set 函数,然后将该对象推回 Stack。当 employee close 元素被解析时,您将完成的对象弹出 Stack,并将其添加到一个 List 对象。当整个文件被解析时,您使用 PersistenceManager 持久化 List 中的每个对象。

根据 “Using the DataStore with JDO” 指南,创建 PersistentManager 就 CPU 时间而言是非常昂贵的。该指南建议您在应用程序启动时使用一个静态最终变量来创建这个对象,然后根据需要获取该对象。

清单 4 展示了 PersistenceManagerFactory 类。注意,这个相同的类,只要更改包名和类名,就可以在您的每个 GAE 项目中使用。

清单 4. XMLImportPersistenceManagerFactory.java
package com.xmlimport;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

public final class XMLImportPersistenceManagerFactory {

    private static final PersistenceManagerFactory pmfInstance = JDOHelper.
        getPersistenceManagerFactory("transactions-optional");

    private XMLImportPersistenceManagerFactory() {}

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
}

最后,您需要创建一个 servlet 来调用 EmployeeHandler 并创建一些 Employee 对象。然后,我们将这个 servlet 定义添加到 web.xml 文件,以便 /CreateEmployee URL 将重定向到它。清单 5 展示了 web.xml 文件。

清单 5. web.xml
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
    <servlet>
    <servlet-name>CreateEmployeeServlet</servlet-name>
    <servlet-class>com.xmlimport.servlet.CreateEmployeeServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>CreateEmployeeServlet</servlet-name>
    <url-pattern>/CreateEmployee</url-pattern>
    </servlet-mapping>
</web-app>

清单 6 展示了这个 servlet。

清单 6. CreateEmployeeServlet.java
package com.xmlimport.servlet;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.xmlimport.employee.EmployeeHandler;

public class CreateEmployeeServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

        new EmployeeHandler();
        PrintWriter out = response.getWriter();
        out.println(“Employees Created”);


    }

	
}

将这些文件添加到一个标准 GAE Java 项目并启动测试服务器。在一个浏览器中输入 http://localhost:8080/CreateEmployee/,稍等片刻,消息 Employees Created 将出现在屏幕上。如果您在 http://localhost:8080/_ah/admin/datastore 处打开本地数据存储,将会看到新创建的 Employee 对象。

手动持久化输入的 XML 数据

这种解决方案对一个已部署站点不是非常实用。要新建一组 Employee 对象,需要创建 employees.xml 文件并每次都将其部署到 appspot.com。我们稍微更改一下这个处理程序的行为。我们将这个处理程序更改为解析来自这个 servlet 中的一个表单的文本输入,而不是解析一个现有文件。

首先更改这个 servlet 来打开一个 Java Server Page (JSP),该页面包含一个文本区域输入框和一个提交按钮。与前面完全相同,文本将被解析,每个新的 Employee 对象都将通过 PersistenceManager 持久化。

doGet 方法更改为重定向到一个名为 createEmployee.jsp 的 JSP,如 清单 7 所示。

清单 7. EmployeeServlet.java 中重构的 Refactored doGet 方法
public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException {

    RequestDispatcher view = request.
        getRequestDispatcher("/createEmployee.jsp");
    view.forward(request, response);
		}

在 war 目录的 root 文件夹中,创建 createEmployee.jsp 文件,如 清单 8 所示:

清单 8. createEmployee.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
    "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Create all employee</title>
</head>
<body>

    <form action="/CreateEmployee" method="POST">
        <textarea name="employeeXML" cols="25" rows="25">
        </textarea>
        <input type="submit" value="Create Employee(s)"/>
    </form>
</body>
</html>

由于表单使用 POST 方法,需要向 CreateEmployeeServlet 类添加 doPost() 函数(参见 清单 9),以便在用户单击 Create Employee(s) 按钮时调用。

清单 9. 将 doPost 方法添加到 EmployeeServlet.java
public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException {

    new EmployeeHandler(request.getParameter("employeeXML");
    RequestDispatcher view = request.
        getRequestDispatcher("/createEmployee.jsp");
    view.forward(request, response);

}

在这个方法中,您提取在表单的文本区域中输入的文本,然后将文本发送到 EmployeeHandler。当文本被解析且 Employee 对象被创建后,将用户重定向回包含一个空白表单的相同页面。

要解析文本,对 EmployeeHandler 的构造函数进行最后一次更改,以接受来自文本区域的文本,而不是解析来自一个 File 对象的文本。如 清单 10 所示,您将文本添加到一个 StringReader 对象,然后解析这个新对象。

清单 10. 使用 String 参数的 EmployeeHandler 构造函数
public EmployeeHandler(String employeeXML) {

    SAXParserFactory factory = SAXParserFactory.newInstance();
    try {

        pm = XMLImportPersistenceManagerFactory.get().getPersistenceManager();
        SAXParser saxParser = factory.newSAXParser();
        saxParser.parse(new InputSource (new StringReader(employeeXML)), this);
        pm.makePersistentAll(employees);

    } catch (Throwable t) {

        t.printStackTrace();

    }
    finally {

        pm.close();

    }

}

现在,您可以将任意 employee 文件添加到这个 web 应用程序,而不必首先上传到 appspot.com。

使用 web 服务来上传 XML 数据

这个解决方案受到两个限制:一是文本区域中可以输入的最大字符数;二是 Google 对发送到 GAE 的请求实施的 30 秒超时。如果文档没有被解析且对象在 30 秒内进行持久化,则服务器将抛出一个异常且对象将不会创建。

SOAP 协议允许通过 Internet 发送和接收 XML 消息。要创建每个员工,您将使用运行在 GAE 上的一个 SOAP 服务 — 一次一个。您可以像前面那样重用相同的处理程序类,但不是在服务器上运行它,相反,您将它用作一个客户端。这次不是将 Employee 对象添加到一个 List,而是将相关信息发送到一个 SOAP 服务来在 GAE 上创建相同的对象,然后持久化该对象。

Spring 是由 SpringSource 开发的一个开源应用程序框架,它包含许多模块,其中有一个远程访问框架支持通过支持 RMI、CORBA、以及基于 HTTP 的协议(包括 SOAP)以 RPC 方式导出和导入 Java 对象。

Force.com Web Service Connector (WSC) 是一个高性能 web 服务客户端栈,使用一个流解析器实现。WSC 还简化了 Force.com API(Web Services/SOAP 或 Asynchronous/REST API)的使用。WSC 可用于调用任何实际文档封装的 web 服务。您可以获取一个可用于 GAE 的版本 — 参见 参考资料 中的链接。

cloudwhiz 博客提供了关于如何在 GAE 上实现一个 SOAP web 服务的详细信息(参见 参考资料 中的一个三部分文章系列的链接),该系列最后一部分将介绍如何使用一个 SOAP 服务来创建并持久化一个对象。

首先,您需要使用一个 Web Service Definition Langage (WSDL) 文件来定义一个 web 服务,指定该 web 服务可以处理的对象和操作。注意第 7 行上的 targetNameSpace 定义:http://xmlimport.appspot.com。这将在稍后用作自定义解组器的限定名。

对于本文,您只需要一个服务 CreateEmployeeService 和一个操作 createEmployee。如果您检查 complexType createEmployeeRequest,它本质上是 employees XML 文件中的条目的一个 XML Schema Definition (XSD) ,但 id 除外,因为它是一个元素而不是一个属性。(参见 清单 11。)

清单 11. employeeService.wsdl
<?xml version="1.0"?>
<definitions 
    name="CreateEmployeeService" 
    targetNamespace="http://xmlimport.appspot.com/"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:tns="http://xmlimport.appspot.com/"
    xmlns="http://schemas.xmlsoap.org/wsdl/">
    <types>
        <schema targetNamespace="http://xmlimport.appspot.com/" 
            xmlns="http://www.w3.org/2001/XMLSchema">
            <element name="createEmployeeRequest" 
                 type="tns:createEmployeeRequest"/>
            <element name="createEmployeeResponse" 
                 type="tns:createEmployeeResponse"/>
            <element name="getEmployeeRequest" 
                 type="tns:getEmployeeRequest"/>
            <element name="getEmployeeResponse" 
                 type="tns:getEmployeeResponse"/>
            <complexType name="createEmployeeRequest">
                <sequence>
                    <element name="firstName" type="string"/>
                    <element name="surName" type="string"/>
                    <element name="emailAddress" type="string"/>
                    <element name="hireDate" type="date"/>
                    <element name="id" type="int"/>
                </sequence>
            </complexType>
            <complexType name="createEmployeeResponse">
                <sequence>
                    <element name="success" type="boolean"/>
                </sequence>
            </complexType>
            <complexType name="getEmployeeRequest">
                <sequence>
                    <element name="successful" type="boolean"/>
                    <element name="firstName" type="string"/>
                    <element name="surName" type="string"/>
                    <element name="emailAddress" type="string"/>
                    <element name="hireDate" type="date"/>
                    <element name="id" type="long"/>
                </sequence>
            </complexType>
            <complexType name="getEmployeeResponse">
                <sequence>
                    <element name="id" type="long"/>
                </sequence>
            </complexType>
        </schema>
    </types>
    <message name="createEmployeeRequest">
        <part name="parameters" element="tns:createEmployeeRequest"/>
    </message>
    <message name="createEmployeeResponse">
        <part name="parameters" element="tns:createEmployeeResponse"/>
    </message>
    <message name="getEmployeeRequest">
        <part name="parameters" element="tns:getEmployeeRequest"/>
    </message>
    <message name="getEmployeeResponse">
        <part name="parameters" element="tns:getEmployeeResponse"/>
    </message>
    <portType name="EmployeeService">
        <operation name="createEmployee">
            <input message="tns:createEmployeeRequest"></input>
            <output message="tns:createEmployeeResponse"></output>
        </operation>
        <operation name="getEmployee">
            <input message="tns:getEmployeeRequest"></input>
            <output message="tns:getEmployeeResponse"></output>
        </operation>
    </portType>
    <binding name="EmployeeServicePortBinding" type="tns:EmployeeService">
        <soap:binding style="document" 
              transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="createEmployee">
            <soap:operation soapAction=""/>
            <input>
                <soap:body use="literal"></soap:body>
            </input>
            <output>
                <soap:body use="literal"></soap:body>
            </output>
        </operation>
        <operation name="getEmployee">
            <soap:operation soapAction=""/>
            <input>
                <soap:body use="literal"></soap:body>
            </input>
            <output>
                <soap:body use="literal"></soap:body>
            </output>
        </operation>
    </binding>
    <service name="CreateEmployeeService">
        <documentation>Create Employee Service</documentation>
        <port name="EmployeeServicePort" 
              binding="tns:EmployeeServicePortBinding">
            <soap:address location="http://localhost:8080/soap/"/>
        </port>
    </service>
</definitions>

使用 Force.com WSC 的 GAE 版本来从 WSDL 构建一个 jar 文件(包含必要的类来发送和接收 SOAP 消息)并将它们添加到 lib 文件夹,使用以下命令:

java -classpath wsc-gae-16_0.jar com.sforce.ws.tools.wsdlc <WSDL input file> 
              <JAR output file>

将创建的 output jar 文件和 wsc-gae.jar 文件添加到您的项目的 lib 文件夹。

下载 Spring 框架 jar 文件(参见 参考资料 中的链接)。

将以下 jar 文件添加到您的项目的 lib 文件夹:

  • org.springframework.aop.jar
  • org.springframework.asm.jar
  • org.springframework.aspects.jar
  • org.springframework.beans.jar
  • org.springframework.context.jar
  • org.springframework.context.support.jar
  • org.springframework.core.jar
  • org.springframework.expression.jar
  • org.springframework.instrument.jar
  • org.springframework.instrument.tomcat.jar
  • org.springframework.jdbc.jar
  • org.springframework.jms.jar
  • org.springframework.orm.jar
  • org.springframework.oxm.jar
  • org.springframework.test.jar
  • org.springframework.transaction.jar
  • org.springframework.web.jar
  • org.springframework.web.portlet.jar
  • org.springframework.web.servlet.jar
  • org.springframework.web.struts.jar
  • spring-oxm.jar
  • spring-oxm-tiger.jar
  • spring-ws-core.jar
  • spring-ws-core-tiger.jar
  • spring-ws-security.jar
  • spring-ws-support.jar
  • spring-xml.jar

研究本文时,我使用的是 SpringSource 3.0 的最终版本,而且没有测试任何更新版本。

GAE 不允许写入本地文件系统,但那是 Spring AxiomSoapMessageFactory 类的先决条件。幸运的是,一个简单的解决方法是扩展这个类并覆盖 afterPropertiesSet() 方法,如 清单 12 所示。这将满足 Spring 的写权限检查要求。

清单 12. 自定义 AxiomSoapMessageFactory 类
package com.xmlimport.service.soap;
import org.springframework.ws.soap.axiom.AxiomSoapMessageFactory;
public class XMLImportMessageFactory extends AxiomSoapMessageFactory {

    public void afterPropertiesSet() throws Exception {
    // Do nothing. 
    // This is because the method checks for write access, which GAE does not allow
	}
}

为了使用 Force.com WSC,您需要一个自定义编组器和解组器,如 清单 13 所示。

清单 13. 自定义 Marshaller 类
package com.xmlimport.service.soap;
public class EmployeeServiceMarshaller extends TransformerObjectSupport 
        implements Marshaller, Unmarshaller {

        public final void marshal(Object graph, Result result) 
            throws XmlMappingException, IOException {

        try {
                    XMLizable xmlObject = (XMLizable)graph;
                    ByteArrayOutputStream xmlBuffer = new ByteArrayOutputStream();

                    // Assumes all services under same name space at present.
                    QName qName = new QName("http://xmlimport.appspot.com/",
                    StringUtils.
                    uncapitalize(xmlObject.getClass().getSimpleName()));

            // Use the Force.com WSC API to generate the XML from the given object.
            XmlOutputStream xout = new XmlOutputStream(xmlBuffer, true);
            xout.startDocument();
            xmlObject.write(qName, xout, new TypeMapper());
            xout.endDocument();
            xout.close();

            // Setup an XMLStreamReader to parse the generated XML buffer.
            XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
          XMLStreamReader xmlStreamReader = 
        xmlInputFactory.createXMLStreamReader(new StringReader(xmlBuffer.toString()));

    	// Copy the contents of the XMLStreamReader into the StaxResult.
    	XMLStreamWriter xmlStreamWriter = 
                                    ((StaxResult)result).getXMLStreamWriter();
    	org.apache.axiom.om.util.CopyUtils.reader2writer(xmlStreamReader,
                    xmlStreamWriter);
        }
        catch (XMLStreamException xse) {

                throw new MarshallingFailureException(
                        "Failed to copy generated object XML into StaxResult.", xse);
	    
        }
}

    public final Object unmarshal(Source source) throws XmlMappingException, IOException {
        XMLizable xmlObject = null;

        if (source != null) {
            try {
             XMLStreamReader xmlStreamReader = ((StaxSource)source).getXMLStreamReader();
                xmlStreamReader.next();

                // Use localName of top element to work out the name of the java class.
                StringBuilder className = new StringBuilder("com.sforce.soap.");
                className.append(StringUtils.capitalize(xmlStreamReader.getLocalName()));
       
                // Create an instance of this class to bind to the XML.
                xmlObject = (XMLizable)Class.forName(className.toString()).newInstance();
       
                // Transform the StaxSource into a StreamResult 
                //so that we get the XML String.
                StringWriter out = new StringWriter();       
                transform(source, new StreamResult(out));

                // Use the XML String with the Force.com WSC 
                //to populate the properties of the object. 
                XmlInputStream xin = new XmlInputStream();
                xin.
                        setInput(new ByteArrayInputStream(out.toString().getBytes()),
                                    "UTF-8");
                xmlObject.load(xin, new TypeMapper());
                } catch (ClassNotFoundException cnfe) {
                        throw new UnmarshallingFailureException(
        "A Force.com WSC generated class was not found that matches the XML message.",
                                    cnfe);
                } catch (IllegalAccessException iae) {
                        throw new UnmarshallingFailureException(
                "Failed to instantiate instance of the Force.com WSC generated class.",
                                    iae);
                } catch (InstantiationException ie) {
                        throw new UnmarshallingFailureException(
                "Failed to instantiate instance of the Force.com WSC generated class.");
                } catch (ConnectionException ce) {
                        throw new UnmarshallingFailureException(
                "Failed to parse XML String using Force.com pull parser.", ce);
                } catch (PullParserException ppe) {
                        throw new UnmarshallingFailureException(
                "Failed to parse XML String using Force.com pull parser.", ppe);
                } catch (TransformerException te) {
                        throw new UnmarshallingFailureException(
                "Failed to transform StaxSource to StreamResult.", te);
                } catch (XMLStreamException xse) {
                        throw new UnmarshallingFailureException(
                "Failed to parse top level element in message payload.", xse);
                }
        }

        return xmlObject;
    }

    /**
     * Assumes that all marshalling and unmarshalling is handled by this implementation.
     */
    @SuppressWarnings("unchecked")
        public boolean supports(Class clazz) {
        return true;
    }
}

这个编组函数用于将来自 web 服务的结果转换为一个 javax.xml.transform.Result。注意,这个编组函数中的 qName 与来自 WSDL 的 targetNameSpace 相同。

解组代码的作用正好相反 — 将通过 HTTP 接收的 javax.xml.transform.Source 转换为一个将被 web 服务使用的 Java 对象。将被创建的类的名称从存储在 source 参数中的 XML 提取。使用 WSC 代码 com.sforce.soap 创建的任何类的包的默认名称将作为前缀附加到这个名称以创建该类的完全限定名(例如 com.sforce.soap.CreateEmployeeRequest)。Reflect 用于创建这个类的一个实例,最后,它的属性使用 source 中的 xml 的剩余部分设置。

supports 函数规定,该服务将处理所有类。

清单 14 所示创建 Spring 框架配置文件 ws-servlet.xml,并将其放置到 GAE 项目的 war 目录中的 WEB-INF 文件夹中。这个文件包含该服务的类名,类使用的编组器和解组器,服务的 WSDL 的位置,以及此前创建的自定义 MessageFactory(参见 清单 12)。确保相同文件夹中的 WSDL 文件如服务 bean(employeeService)的 constructor-arg 元素一样定义。这个文件与 web.xml 文件一起放置到项目的 WEB-INF 文件夹中。

清单 14. ws-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean class="org.springframework.ws.server.endpoint.mapping.SimpleMethodEndpointMapping">
        <property name="endpoints" ref="createEmployeeService"></property>
    </bean>

<bean id="createEmployeeService" class="com.xmlimport.service.soap.CreateEmployeeService">
        <property name="marshaller" ref="createEmployeeServiceMarshaller" />
        <property name="unmarshaller" ref="createEmployeeServiceUnmarshaller" />
    </bean>

<bean id="createEmployeeServiceMarshaller" 
class="com.xmlimport.service.soap.EmployeeServiceMarshaller"></bean>

<bean id="createEmployeeServiceUnmarshaller" 
class="com.xmlimport.service.soap.EmployeeServiceMarshaller"></bean>

<bean id="employeeService" 
    class="org.springframework.ws.wsdl.wsdl11.SimpleWsdl11Definition">
        <constructor-arg value="/WEB-INF/wsdl/employeeService.wsdl"/>
    </bean>

<bean id="messageFactory" class="com.xmlimport.service.soap.XMLImportMessageFactory">
        <property name="payloadCaching" value="false"/>
        <property name="attachmentCaching" value="false"/>
    </bean>

</beans>

将这个 servlet 定义添加到 web.xml(如 清单 15 所示),以便对 /soap/* URI 的所有请求将被传递到 Spring 框架,并从那里传递到在框架配置中的定义的服务类。servlet 名称是 ws,并且,根据惯例,框架配置文件是 <servlet-name>-servlet.xml,因此上一步中的名称为 ws-servlet.xml

清单 15. SOAP servlet 定义 web.xml
<servlet>
    <servlet-name>ws</servlet-name>
<servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet
                                                                   </servlet-class>
    <init-param>
        <param-name>transformWsdlLocations</param-name>
        <param-value>true</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>ws</servlet-name>
    <url-pattern>/soap/*</url-pattern>
</servlet-mapping>

</web-app>

最后,编写 web 服务类本身。由于每个请求都被解组函数接收,您创建对应的对象并将其传递到服务中的相关函数。在 handlecreateEmployeeRequest 函数中,您提取必要的信息,以便从 CreateEmployeeRequest 对象创建一个 Employee 对象,然后持久化新对象。注意,这个函数的名称是 handlecreateEmployeeRequest(包含一个小写 “c”),而不是 handleCreateEmployeeRequest(按照 Java 语言惯例使用大写 “C”)。

编写服务类 CreateEmployeeService(参见 清单 16)。当每个请求被收到时,创建一个 Employee 对象的必要信息从 CreateEmployeeRequest 对象中提取,然后持久化新对象。注意,函数名称是 handlecreateEmployeeRequest(使用 “create” 的小写 “c”)。

清单 16. CreateEmployeeService.java
package com.xmlimport.service.soap;

import org.springframework.ws.server.endpoint.adapter.MarshallingMethodEndpointAdapter;

import com.sforce.soap.CreateEmployeeRequest;
import com.sforce.soap.CreateEmployeeResponse;
import com.xmlimport.employee.Employee;
import javax.jdo.PersistenceManager;
import com.xmlimport.XMLImportPersistenceManagerFactory;

public class CreateEmployeeService extends MarshallingMethodEndpointAdapter {

    Logger log = Logger.getLogger(CreateEmployeeService.class.getName());

    public CreateEmployeeResponse handlecreateEmployeeRequest(CreateEmployeeRequest 
                                                                 createEmployeeRequest) {

        Employee employee = new Employee();
        employee.setFirstName(createEmployeeRequest.getFirstName());
        employee.setSurName(createEmployeeRequest.getSurName());
        employee.setEmailAddress(createEmployeeRequest.getEmailAddress());
        employee.setId(createEmployeeRequest.getId());
        employee.setHireDate(createEmployeeRequest.getHireDate().getTime());
PersistenceManager pm = XMLImportPersistenceManagerFactory.get().getPersistenceManager();
        try {

            pm.makePersistent(employee);			

        }
        finally {

            pm.close();

        }
        CreateEmployeeResponse createEmployeeResponse = new CreateEmployeeResponse();
        createEmployeeResponse.setSuccess(true);

        return createEmployeeResponse;

    }

}

编译项目并将其部署到 appspot.com。确保可以从一个浏览器访问 application <application name>.appspot.com/soap/wsdl/employeeServices.wsdl 的 WSDL。我使用 soapUI 工具(参见 参考资料 中的下载链接)来测试 SAOP 和 REST 服务。使用 soapUI 从与前面相同的 URL 打开 WSDL 文件,soapUI 将使用 firstName、surName 等的空元素创建 SOAP 请求消息。为这些元素输入一些值并单击绿色箭头提交请求。响应应该会使 success 元素设置为 true。从 GAE 应用程序指示板,使用 Data Viewer 确认项目已创建。

从一个 XML 文档批量上传

尽管 soapUI 能够很好地创建单一条目,在 employees XML 文件中创建每个条目,但您需要一个客户端程序来解析文件并使用每个条目调用服务。这个客户端使用 web 服务创建过程中创建的相同 jar 文件,但使用 WSC jar 文件的客户端版本而不是特定于 GAE 的版本。

清单 17 是 EmployeeHandler 的新版本。

清单 17. EmployeeHandler.java
package com.xmlimport.client;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.sforce.soap.Connector;
import com.sforce.soap.SoapConnection;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;

public class EmployeeHandler extends DefaultHandler {

    private static final Logger log=Logger.getLogger(EmployeeHandler.class.
            getName());
    private static final SimpleDateFormat hireDateFormat = 
                    new SimpleDateFormat("yyyy-MM-dd");

    private Stack<Employee> employeeStack;
        private String characters;

        public EmployeeHandler() {

                    SAXParserFactory factory = SAXParserFactory.newInstance();
                    try {

                        SAXParser saxParser = factory.newSAXParser();
                        saxParser.parse(new InputSource("./employees.xml"), this);

                    } catch (Throwable t) {

                        t.printStackTrace();

                    }

        }

    public void startDocument() 
                    throws SAXException {	

                    employeeStack = new Stack<Employee>();
		
    }

        public void startElement(String namespaceURI, 
                                                String localName,
                                                String qualifiedName,
                                                Attributes attributes) 
                    throws SAXException {

                    if (qualifiedName.equals("employee")) {

                        Employee employee = new Employee();
                        employee.setEmployeeId(new Integer(attributes.getValue("id")));
                        employeeStack.push(employee);

                    }

        }

        public void endElement(String namespaceURI, 
                                                String simpleName,
                                                String qualifiedName) 
                    throws SAXException {

                    if (!employeeStack.isEmpty()) {

                        if (qualifiedName.equals("employee")) {

                                sentEmployeeSOAPMessage(employeeStack.pop());

                        } 
                        else if (qualifiedName.equals("firstName")) {

                                Employee employee = employeeStack.pop();
                                employee.setFirstName(characters);
                                employeeStack.push(employee);

                        } 
                        else if (qualifiedName.equals("surName")) {

                                Employee employee = employeeStack.pop();
                                employee.setSurName(characters);
                                employeeStack.push(employee);

                        } 
                        else if (qualifiedName.equals("emailAddress")) {

                                Employee employee = employeeStack.pop();
                                employee.setEmailAddress(characters);
                                employeeStack.push(employee);

                        } 
                        else if (qualifiedName.equals("hireDate")) {

                                Employee employee = employeeStack.pop();
                                try {

                                        employee.setHireDate(
                                                 hireDateFormat.parse(characters));

                                } 
                                catch (ParseException e) {

                                        log.log(Level.FINE, 
                                                 "Could not parse date {0}", characters);
                                }
                                employeeStack.push(employee);

                        }

                    }

    }

    public void characters(char buf[], int offset, int len) 
                    throws SAXException {

                    characters = new String(buf, offset, len);

    }

    private void sentEmployeeSOAPMessage(Employee employee) {

                    ConnectorConfig config = new ConnectorConfig();
                    config.setServiceEndpoint("http://xmlimport.appspot.com/soap/");
                    Calendar hireDate = Calendar.getInstance();
                    hireDate.setTime(employee.getHireDate());
                    System.out.println("Creating employee " + 
                                employee.getFirstName() + " " + 
                                employee.getSurName());
                    try {
                        SoapConnection soapConnection = Connector.newConnection(config);
                        boolean success = soapConnection.createEmployee(
                                        employee.getFirstName(), 
                                        employee.getSurName(), 
                                        employee.getEmailAddress(),
                                        hireDate, 
                                        employee.getEmployeeId());
                        System.out.println(success?"Success":"Failure");
                    } catch (ConnectionException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }

    }


    public static void main(String[] args) {

                    new EmployeeHandler();

    }

}

当您解析每个条目时,您将一条消息打印到标准输出,显示当前正在创建的员工的姓名,以及创建是成功还是失败。

这个解决方案没有与前面的解决方案相同的超时限制,但如果客户端 XML 文件足够大,创建和持久化每个对象可能会超出 appspot.com 服务器上的 CPU 时间的日常配额。

结束语

本文演示了从一个 XML 文档中的数据创建对象,并将它们持久化到 GAE 开发人员可用的 DataStore 上的各种方法。使用最后的、基于 SOAP 的客户端和服务器方法,Java 开发人员现在可以批量上传 XML 数据。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=550040
ArticleTitle=将 XML 数据导入 Google App Engine
publish-date=10112010