Импортирование XML-данных в Google App Engine

Как загрузить большой объем данных, хранящихся локально в XML-файлах, в постоянную базу данных объектов Google App Engine

Служба Google App Engine открылась в апреле 2008 года и предлагала способ загрузки больших объемов данных, хранящихся в файлах CSV, с использованием языка программирования Python. Поддержка Java™ была добавлена годом позже. На сегодняшний день в App Engine отсутствует встроенная поддержка массовых загрузок для Java, и единственной средой хранения данных, которая поддерживает инструмент массовой загрузки, остается CSV. Эта статья знакомит читателя с различными методами сохранения данных из XML-документов в постоянной базе данных App Engine.

Джозеф P. Маккарти, программист, IBM

Фотография Джозефа МаккартиДжозеф Маккарти (Joseph McCarthy) работает Java-программистом в лаборатории программного обеспечения Дублине. Пришел в IBM в июле 2002 года после окончания университета Лимерика со степенью бакалавра компьютерных наук и дипломом в инженера вычислительной техники.



27.07.2011

Общие сведения

В апреле 2008 года компания Google открыла бесплатную службу хостинга Web-приложений Google App Engine (GAE) (см. ссылку в разделе Ресурсы). Сначала она поддерживала только приложения, написанные на языке Python, но в апреле 2009 года добавилась поддержка языка Java.

Часто используемые сокращения

  • API: Application programming interface – интерфейс прикладных программ
  • CPU: центральный процессор
  • CSV: Comma-separated value – значения, разделенные запятыми
  • HTML: HyperText Markup Language - язык разметки гипертекста
  • IDE: интегрированная среда разработки
  • REST: REpresentational State Transfer - передача репрезентативного состояния
  • SAX: Simple API for XML - простой API для XML
  • SOAP: Simple Object Access Protocol - простой протокол доступа к объектам
  • UI: User Interface – интерфейс пользователя
  • URI: Uniform Resource Identifier ― универсальный идентификатор ресурса
  • URL: Uniform Resource Locator - универсальный указатель ресурса
  • WSC: Web Services Connector
  • WSDL: Web Services Description Language – язык описания Web-сервисов
  • W3C: World Wide Web Consortium
  • XML: Extensible Markup Language – расширяемый язык разметки

Прилагаемая среда разработки приложений создает локальную базу данных для хранения данных в процессе разработки, а сам сайт позволяет хранить данные в качестве постоянных объектов, или структур (entities). Эти структуры создаются с помощью объектов Plain Old Java Objects (POJO), снабженных аннотациями Java Data Object (JDO). Тем не менее, среда не обеспечивает возможности обмениваться данными непосредственно между двумя базами данных (локальной и удаленной). Среда Python позволяет выполнять массовую загрузку данных, хранящихся в формате CSV. Официально она не поддерживает загрузку больших объемов данных на языке Java. Рекомендуемый метод заключается в загрузке данных с помощью Python-версии приложения и доступе к данным с помощью Java-классов, но для этого требуется знание Python и умение представить данные в формате CSV.

XML ― это гибкий текстовый формат. В последние годы онлайн- и офлайн-приложения все чаще хранят данные в XML для самых разнообразных целей. Несмотря на распространенность XML в Интернете, GAE не обеспечивает массовой загрузки данных, хранящихся в XML-документах.

SAX – это API парсера последовательного доступа для XML. Если написать основанный на SAX парсер для XML-документов, можно использовать несколько методов обратного вызова, которые запускаются, когда сталкиваются с различными элементами документа при разборе документа (например, начало документа, начало XML-элемента, конец элемента, определенные символы и т.п).


Простая XML-персистентность

Простейший метод добавления данных из XML-документа в хранилище GAE ― это загрузка документа в рамках приложения и использование специального SAX-парсера для создания в приложении класса, основанного на каждой записи в документе.

Возьмем простой XML-документ, содержащий список сотрудников организации, добавим его в проект GAE, создадим класс элементов и сохраним их в качестве структур.

XML-документ employees.xml имеет простой формат (см. листинг 1). Каждому сотруднику соответствует один атрибут (id) и четыре элемента: firstName, surName, emailAddress и hireDate.

Файл 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.javaАннотированный 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() {

    }
}

Чтобы создать SAX Parser на языке Java, расширим класс 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 и добавляется в стек. При разборе любых других элементов объект Employee извлекается из стека, вызывается соответствующая функция set, и объект возвращается обратно в стек. При обработке элемента employee close объект извлекается из стека и добавляется в объект List. При анализе всего документа целиком каждый объект помещается в List с помощью PersistenceManager.

Как утверждается в руководстве "Using the DataStore with JDO" (Использование DataStore с помощью JDO), создание объекта PersistentManager довольно затратно в плане процессорного времени. Руководство рекомендует использовать статическую переменную final, чтобы создать объект один раз при запуске приложения, а затем извлекать его по мере необходимости.

В листинге 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;
    }
}

Наконец, нужно создать сервлет для вызова EmployeeHandler и создания объектов Employee. Добавьте определение сервлета в файл web.xml, чтобы переадресовать на него URL /CreateEmployee. В листинге 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 показан сервлет.

Листинг 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”);
    }

	
}

Добавьте эти файлы в стандартный проект Java GAE и запустите тестовый сервер. Введите в браузер http://localhost:8080/CreateEmployee/, и вскоре на экране появится сообщение Employees Created (сотрудники созданы). Если открыть окно просмотра локального хранилища по адресу http://localhost:8080/_ah/admin/datastore, можно увидеть вновь созданные объекты Employee.


Сохранение введенных XML-данных вручную

Это решение не очень практично для целей развертывания сайта. Чтобы создать новый набор объектов Employee, необходимо каждый раз создавать файл employees.xml и развертывать его на appspot.com. Давайте немного изменим поведение обработчика. Вместо анализа существующего файла, он будет разбирать вводимый текст из формы в сервлете.

Сначала изменим сервлет, открыв страницу Java Server Page (JSP) с формой, содержащей окно ввода текста и кнопку отправки. Нажмите кнопку, чтобы отправить текст, введенный в поле ввода, в EmployeeHandler. Как и прежде, текст будет анализироваться, и PersistenceManager сделает каждый новый объект Employee постоянным.

Изменим метод doGet, чтобы перенаправить JSP с именем createEmployee.jsp, как показано в листинге 7 .

Листинг 7. Измененный метод doGet в EmployeeServlet.java
public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException {

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

В корневой папке каталога war создайте файл 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, он принимал его из текстового поля. Добавим текст в объект StringReader и проанализируем этот новый объект, как показано в листинге 10.

Листинг 10. Конструктор EmployeeHandler, использующий параметр String
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();

    }

}

Теперь можно добавлять любые файлы сотрудников в веб-приложение без необходимости предварительно загружать их в appspot.com.


Использование Web-сервисов для загрузки XML-данных

Это решение ограничено как максимальным числом символов, которые можно ввести в текстовое поле, так и 30-секундным интервалом, отведенным Google для запросов к GAE. Если документ не проанализирован, и объекты не сделаны постоянными в течение 30 секунд, сервер выдаст исключение, и объекты созданы не будут.

SOAP ― это протокол, который позволяет отправлять и получать XML-сообщения через Интернет. Для создания каждого сотрудника мы всякий раз будем использовать SOAP-службу, работающую на GAE. Можно использовать тот же самый обработчик класса, но вместо того чтобы запускать его на сервере, мы будем использовать его в качестве клиента. А вместо того чтобы добавлять в список объект Employee, соответствующая информация направляется в сервис SOAP для создания того же объекта в GAE с последующим его сохранением.

Spring – это разработанная SpringSource среда приложений с открытым исходным кодом, содержащая (среди прочих модулей) среду дистанционного доступа, которая, подобно RPC, позволяет экспортировать и импортировать объекты Java по сетям, поддерживающим RMI, CORBA и HTTP-протоколы, включая SOAP.

Web Service Connector (WSC) от Force.com ― это высокопроизводительный клиентский стек Web-сервисов, использующий потоковый парсер. WSC также значительно облегчает использование API Force.com (Web-сервисы/SOAP или асинхронный/REST API). WSC может использоваться для вызова любого Web-сервиса в оболочке doc/literal. Имеется версия для использования с GAE - см. ссылку в разделе Ресурсы.

В блоге cloudwhiz содержатся сведения о том, как реализовать Web-сервис SOAP в GAE (см. ссылку на статью из трех частей в разделе Ресурсы), и заключительная часть этой статьи посвящена тому, как использовать сервис SOAP для создания объекта и его сохранения.

Прежде всего необходимо определить Web-сервис, используя файл Web Service Definition Langage (WSDL). Он определяет объекты и операции, которыми можно управлять с помощью Web-сервиса. Обратите внимание на определение TargetNamespace в строке 7: http://xmlimport.appspot.com. Оно будет использоваться в дальнейшем как допустимое имя специального демаршаллера.

Для целей данной статьи требуется только один сервис, CreateEmployeeService, и одна операция, createEmployee. Если рассмотреть ComplexType createEmployeeRequest, то это в основном определение XML-схемы (XSD) записей сотрудников в XML-файле, за исключением того, что 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>

Воспользуйтесь GAE-версией WSC от Force.com для создания jar-файла из WSDL (с необходимыми классами для отправки и получения сообщений SOAP) и добавьте то и другое в папку lib с помощью команды:

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

Добавьте созданный выходной файл jar и файл wsc-gae.jar в папку lib своего проекта.

Загрузите файлы jar среды Spring Framework (см. ссылку в разделе Ресурсы ).

Добавьте в папку lib проекта следующие файлы jar:

  • 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 {
    // Ничего не делает. 
    // Это из-за проверки  методом наличия попыток
     доступа для записи, которые GAE не допускает
	}
}

Для того чтобы использовать WSC от Force.com, необходимо настроить маршаллер и демаршаллер, как показано в листинге 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();

                    // Предполагается, что на данный момент все сервисы находятся 
                    // в одном и том же пространстве имен.
                    QName qName = new QName("http://xmlimport.appspot.com/",
                    StringUtils.
                    uncapitalize(xmlObject.getClass().getSimpleName()));

            // Для создания XML из данного объекта используется API WSC от Force.com.
            XmlOutputStream xout = new XmlOutputStream(xmlBuffer, true);
            xout.startDocument();
            xmlObject.write(qName, xout, new TypeMapper());
            xout.endDocument();
            xout.close();

            // Установка XMLStreamReader для разбора сгенерированного буфера XML.
            XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
          XMLStreamReader xmlStreamReader = 
        xmlInputFactory.createXMLStreamReader(new StringReader(xmlBuffer.toString()));

    	// Копирование содержимого XMLStreamReader в 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();

                // Использование LocalName верхнего элемента для составления имени 
                // java-класса.
                StringBuilder className = new StringBuilder("com.sforce.soap.");
                className.append(StringUtils.capitalize(xmlStreamReader.getLocalName()));

                // Создание экземпляра этого класса для привязки к XML.
                xmlObject = (XMLizable)Class.forName(className.toString()).newInstance();

                // Преобразование StaxSource в StreamResult 
                //so that we get the XML String.
                StringWriter out = new StringWriter();       
                transform(source, new StreamResult(out));

                // Использование XML String с WSC от Force.com  
                //для заполнения свойств объекта. 
                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;
    }

    /**
     * Предполагается, что весь маршаллинг и демаршаллинг выполняется в этой реализации.
     */
    @SuppressWarnings("unchecked")
        public boolean supports(Class clazz) {
        return true;
    }
}

Функция marshall используется для преобразования результатов, полученных от Web-сервиса, в javax.xml.transform.Result Обратите внимание, что имя QName в функции marshal то же, что и в TargetNamespace из WSDL.

Код unmarshall используется для обратного преобразования - javax.xml.transform.Source, полученного через HTTP, в объект Java, который будет использоваться Web-сервисом. Имя создаваемого класса извлекается из XML-файла, хранящегося в параметре source. Для создания допустимого имени класса к этому добавляется имя по умолчанию пакета любого класса, созданного с помощью кода WSC, com.sforce.soap (например, com.sforce.soap.CreateEmployeeRequest). Reflect используется для создания экземпляра этого класса и, наконец, с помощью оставшейся части XML в исходном коде устанавливаются его атрибуты.

Функция supports указывает, что сервис будет обрабатывать все классы.

Создайте файл конфигурации среды Spring, WS-servlet.xml, как в листинге 14, и поместите его в папку WEB-INF каталога war проекта GAE. Этот файл содержит имя класса сервиса, маршаллер и демаршаллер, которые использует этот класс, местоположение WSDL сервиса и специальный класс MessageFactory, который мы создали выше (см. листинг 12). Файл WSDL должен находиться в той же папке, что определена в элементе constructor-arg модуля сервиса (EmployeeService). Этот файл помещается в папку WEB-INF проекта вместе с файлом web.xml.

Листинг 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>

Добавьте определение сервлета в web.xml, как показано в листинге 15, чтобы все запросы к URI /soap/* передавались в среду Spring, а оттуда ― в класс сервиса, определенный в конфигурации среды. Сервлет носит имя ws, а файл конфигурации среды, по соглашению, именуется <servlet-name>-servlet.xml, отсюда и имя ws-servlet.xml на предыдущем шаге.

Листинг 15. Определение сервлета SOAP 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-сервиса. Поскольку каждый запрос получается с помощью функции unmarshall, необходимо создать соответствующий объект и передать его соответствующей функции сервиса. В функции handlecreateEmployeeRequest мы извлекаем необходимую информацию для создания объекта Employee из объекта CreateEmployeeRequest, а затем делаем новый объект постоянным. Обратите внимание, что имя функции handlecreateEmployeeRequest (со строчной "c"), а не handleCreateEmployeeRequest (с прописной "C", как это принято в языке Java).

Напишем класс сервиса CreateEmployeeService (см. листинг 16). При поступлении каждого запроса необходимая информация для создания объекта Employee извлекается из объекта CreateEmployeeRequest, а затем новый объект делается постоянным. Обратите внимание на имя функции handlecreateEmployeeRequest (со строчной "с" в слове "create").

Листинг 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. Проверьте, что вы получили доступ к WSDL для приложения <имя приложения>.appspot.com/SOAP/WSDL/employeeServices.wsdl из браузера. Для тестирования сервисов SOAP и REST я использую инструмент SoapUI (см. ссылку в разделе Ресурсы ). Откройте файл WSDL с помощью SoapUI из того же URL, что и прежде, и SoapUI автоматически создаст сообщения SOAP-запросов с пустыми элементами firstName, surname и т.п. Введите какие-нибудь значения для них и нажмите на зеленую стрелку, чтобы отправить запрос. Ответ должен содержать элемент success со значением true. На приборной панели GAE используйте Data Viewer, чтобы убедиться, что объект создан.


Массовая загрузка из XML-документа

SoapUI прекрасно подходит для создания одной записи, но чтобы создать все записи XML-файла сотрудников, необходима клиентская программа, которая будет анализировать файл и вызывать сервис для каждой записи. Этот клиент использует все тот же файл jar, созданный при построении Web-сервиса, но вместо версии для GAE применяется клиентская версия jar-файла WSC.

В листинге 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
                        e.printStackTrace();
                    }

    }
    public static void main(String[] args) {

                    new EmployeeHandler();

    }

}

При разборе каждой записи на стандартное устройство вывода выводится сообщение с именем созданного сотрудника и указанием того, что создание прошло успешно или закончилось неудачей.

В этом решении нет тех ограничений тайм-аута, что в предыдущих, но если клиентский XML-файл достаточно велик, создание и сохранение каждого объекта может превысить ежедневную норму процессорного времени серверов appspot.com.


Заключение

В этой статье я продемонстрировал различные методы создания объектов из данных XML-документа и их сохранения в хранилище DataStore, доступном для разработчиков GAE. В результате у Java-разработчиков теперь есть метод для SOAP-клиента и сервера, а также метод для загрузки массивов XML-данных.


Загрузка

ОписаниеИмяРазмер
Исходный код приложения и сервисаXMLImport-GAEProject.zip44 КБ
Исходный код SOAP-клиента XMLImportSOAPClient.zip24 КБ

Ресурсы

Научиться

Получить продукты и технологии

  • JDK для GAE (включая плагин Eclipse): загрузите и начните создавать Web-приложения с применением стандартных технологий Java и запускать их в масштабируемой инфраструктуре Google.(EN)
  • Главная страница Spring Framework: загрузите эту платформу для построения и выполнения корпоративных Java-приложений.(EN)
  • Force.com Web Service Connector for GAE: загрузите высокопроизводительный клиентский стек Web-сервисов, реализованный с применением потокового анализатора Google.(EN)
  • wsc-gae-16_0.jar: загрузите GAE-версию Web Service Connector (WSC) от Force.com.
  • Инструмент SoapUI: загрузите инструмент функционального тестирования с открытым исходным кодом, который используется главным образом для тестирования Web-сервисов.
  • WSC-17_0.jar: загрузите автономную версию jar-файла WSC.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML
ArticleID=746530
ArticleTitle=Импортирование XML-данных в Google App Engine
publish-date=07272011