目次


Google App Engine に XML データをインポートする

ローカルの XML ファイルに保存されているバルク・データを Google App Engine の永続オブジェクト・データベースにアップロードする

Comments

背景

Google App Engine (GAE) (「参考文献」のリンクを参照) は、Google が無料で提供する、Web アプリケーションのホスティング・サービスとして、2008年 4月にサービスが開始されました。最初は Python で作成されたアプリケーションのみがサポートされていましたが、2009年 4月に Java 言語のサポートが追加されました。

GAE から提供される、アプリケーションの開発環境では、開発時のデータを永続化するためのローカル・データベースが作成されます。また GAE のサイト自体には、永続オブジェクト、つまりエンティティーとしてデータを格納することができます。これらのエンティティーは、JDO (Java Data Object) アノテーションが付けられた POJO (Plain Old Java Object) を使って作成されます。ただしこの開発環境には、2 つのデータベース (ローカル・データベースとデプロイされたデータベース) の間で直接データをアップロードする手段がありません。Python 環境を使用すれば、CSV フォーマットで保存されているデータをバルク・アップロードすることができますが、この環境では Java 言語で記述したコードを使ってそのままデータをバルク・アップロードする機能を正式にはサポートしていません。そこで、推奨される方法としては、データをアップロードする際には Python で作成したアプリケーションを使用し、そのデータにアクセスする際には Java クラスを使用する方法です。しかしそのためには Python の実用的な知識が必要であり、またデータを CSV フォーマットで表現できる必要があります。

XML はテキスト・ベースの柔軟なフォーマットです。最近のアプリケーションでは、オンライン、オフラインによらず、データを XML として保存し、さまざまな方法で使用することが多くなっています。インターネットの至るところで XML が使われていますが、GAE は XML 文書に保存されているデータをバルク・アップロードするサービスを提供していません。

SAX は XML にシリアル・アクセスするためのパーサー API です。XML 文書用の SAX ベースのパーサーを作成する場合、構文解析中に文書のさまざまな要素を検出するとトリガーされる、いくつかのコールバック・メソッドを使用することができます (検出対象となる要素には、文書の始まり、XML 要素の開始タグと終了タグ、そして文字列などがあります)。

簡単に XML を永続化する

XML 文書のデータを GAE のデータストアに追加するための最も簡単な方法は、その文書をアプリケーションの一部としてアップロードした後、SAX ベースのカスタム・パーサーを使用して、その文書の各エントリーを基にアプリケーションの中にクラスを作成する方法です。

では一例として、ある組織の従業員のリストを含む単純な XML 文書を取り上げます。ここでは、この文書を GAE プロジェクトに追加し、いくつかの要素を扱うための 1 つのクラスを作成し、それらの要素をエンティティーとして保存します。

この XML 文書、employees.xml (リスト 1) は単純なフォーマットをしています。各従業員の要素 (employee 要素) には 1 つの属性 (id) と 4 つの要素 (firstName、surName、emailAddress、hireDate) があります。

employees.xml ファイルはこの記事を通して使用します。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() {

    }
}

Java 言語で SAX パーサーを作成するためには、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> 以外の各要素が構文解析されると、Employee オブジェクトをスタックからポップし、このオブジェクトに関する set 関数を呼び出し、再度 Employee オブジェクトをスタックにプッシュします。employee の close 要素が構文解析されると、完成したオブジェクトをスタックからポップし、List オブジェクトに追加します。文書全体が構文解析されたら、PersistenceManager を使って各オブジェクトを List の中に永続化します。

「JDO を利用したデータストアの使用」ガイドによると、PersistentManager オブジェクトの作成には CPU を非常に長い時間使用します。同ガイドでは、静的な final 変数を使用してアプリケーションの起動時に PersistentManager オブジェクトを作成しておき、必要なときに取得する方法を推奨しています。

リスト 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 ファイルに追加します。これで、/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 に、このサーブレットを示します。

リスト 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 にデプロイする必要があります。そこで、ハンドラーの動作を少し変更し、既存のファイルを構文解析する代わりに、サーブレットの中のフォームに入力されたテキストを構文解析するようにしてみましょう。

まず、textarea 入力ボックスと送信ボタンを含むフォームを持つ JSP (Java Server Page) を開くようにサーブレットを変更し、送信ボタンをクリックすると textarea 入力ボックスに入力されたテキストが EmployeeHandler に送信されるようにします。これにより、先ほどとまったく同じようにテキストが構文解析され、新しい各 Employee オブジェクトが PersistenceManager によって永続化されます。

doGet メソッドを変更し、createEmployee.jsp という JSP にリダイレクトします (リスト 7)。

リスト 7. EmployeeServlet.java の doGet メソッドをリファクタリングする
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) (従業員の作成)」ボタンをクリックすると doPost() 関数が呼び出されるようにする必要があります。

リスト 9. EmployeeServlet.java に doPost メソッドを追加する
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);

}

このメソッドでは、フォームの textarea に入力されたテキストを抽出して EmployeeHandler に送信しています。このテキストが構文解析されて Employee オブジェクトが作成されたら、空のフォームを持つ同じページにユーザーを再びリダイレクトします。

このテキストを構文解析するために、EmployeeHandler のコンストラクターに最終的な変更を加え、File オブジェクトのテキストを構文解析する代わりに textarea のテキストを受け付けるようにします。リスト 10 のように、StringReader オブジェクトにテキストを追加し、この新しい StringReader オブジェクトを構文解析します。

リスト 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 にアップロードしなくても任意の employee ファイルを Web アプリケーションに追加することができます。

Web サービスを使って XML データをアップロードする

このソリューションは、textarea に入力できる最大文字数と、GAE に送信されるリクエストに対して Google が強制する 30 秒のタイムアウトの両方によって制限されています。文書の構文解析とオブジェクトの永続化が 30 秒以内に行われないと、サーバーが例外をスローし、オブジェクトは作成されません。

SOAP はインターネット上で XML メッセージを送受信できるようにするプロトコルです。ここでは各 employee を作成するために、GAE 上で実行される SOAP サービスを employee ごとに使用することにします。先ほどと同じハンドラー・クラスを再利用することができますが、ハンドラー・クラスをサーバー上で実行するのではなく、クライアントとして使用します。Employee オブジェクトを List に追加する代わりに、関連する情報が SOAP サービスに送信され、Employee オブジェクトと同じオブジェクトが GAE 上に作成された後で永続化されます。

Spring は SpringSource によって開発されたオープンソースのアプリケーション・フレームワークです。Spring にはさまざまなモジュールが含まれていますが、リモート・アクセス・フレームワークを使用すれば、RMI、CORBA、そして (SOAP を含む) HTTP ベースのプロトコルをサポートするネットワークを介して、Java オブジェクトを RPC 方式でエクスポート、インポートすることができます。

Force.com の WSC (Web Service Connector) はハイパフォーマンスの Web サービス・クライアント・スタックであり、ストリーミング・パーサーを使って実装されています。また、WSC では Force.com の API (Web サービス/SOAP API または 非同期/REST API) を非常に容易に使えるようになっています。WSC を使用すると、文書リテラルでラップされた任意の Web サービスを呼び出すことができます。GAE で使用可能な WSC のバージョンについては「参考文献」のリンクを参照してください。

Cloud Whiz のブログには、GAE 上で SOAP による Web サービスの実装方法が詳細に説明されています (「参考文献」に 3 回連載の記事へのリンクがあります)。また、本記事の最後の部分では、SOAP サービスを使用してオブジェクトを作成して永続化する方法を説明します。

まず、WSDL (Web Service Definition Language) ファイルを使用して Web サービスを定義する必要があります。このファイルには、Web サービスが扱うことのできるオブジェクトと操作を規定します。7 行目にある targetNameSpace の定義 (http://xmlimport.appspot.com) に注意してください。あとで、この定義をカスタムのアンマーシャラーの修飾名として使います。

この記事で必要なものは、CreateEmployeeService という 1 つのサービスと、createEmployee という 1 つの操作のみです。complexType の createEmployeeRequest を調べてみると、createEmployeeRequest は基本的に employees.xml ファイルのエントリーの XSD (XML Schema Definition) であり、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 から (SOAP メッセージの送受信に必要なクラスを持つ) JAR ファイルを作成します。

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

作成された出力 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 クラスではローカルのファイルシステムへの書き込みが不可欠です。幸い、この問題に対する単純な対策として、AxiomSoapMessageFactory クラスを継承し、afterPropertiesSet() メソッドをオーバーライドして何もしないようにする方法があります (リスト 12)。

リスト 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. カスタムのマーシャラー・クラス
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 を使って作成し、最後に、ソースの中にある XML の他の部分を使ってクラスの属性を設定します。

supports 関数は、このサービスによってすべてのクラスが処理されるように記述しています。

Spring フレームワークの構成ファイル (ws-servlet.xml) を作成し (リスト 14)、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>

web.xml にサーブレットの定義を追加します (リスト 15)。こうすることで、/soap/* という URI へのリクエストはすべて Spring フレームワークに渡され、さらに Spring フレームワークから、Spring フレームワークの構成で定義されたサービス・クラスに渡されるようになります。サーブレットの名前は、慣例により ws であり、Spring フレームワークの構成ファイルは <servlet-name>-servlet.xml です。先ほどのステップで ws-servlet.xml という名前が使われているのは、このためです。

リスト 15. web.xml での SOAP サーブレットの定義
<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 オブジェクトを作成し、この新しい 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 にデプロイします。ブラウザーで <アプリケーション名>.appspot.com/soap/wsdl/employeeServices.wsdl を指定するとアプリケーションの WSDL にアクセスできることを確認します。私は soapUI ツール (ダウンロードするためのリンクは「参考文献」を参照してください) を使って SOAP サービスと REST サービスをテストしました。soapUI を使って先ほどと同じ URL から WSDL ファイルを開くと、firstName、surName 等に対する空要素を持つ SOAP リクエスト・メッセージが soapUI によって自動的に作成されます。これらの要素に何らかの値を入力し、緑色の矢印をクリックしてリクエストを送信します。レスポンスを見ると success 要素が true に設定されているはずです。GAE のアプリケーションのダッシュボードから、オブジェクトが作成されたことを Data Viewer を使って確認します。

XML 文書からのバルク・アップロード

soapUI は 1 つのエントリーを作成するのには適していますが、employees.xml ファイルの各エントリーを作成するためには、そのファイルを構文解析してエントリーごとにサービスを呼び出すクライアント・プログラムが必要です。このクライアントは Web サービスを作成する際に作成されたのと同じ JAR ファイルを使用しますが、GAE 専用のバージョンではなく、WSC の JAR ファイルのクライアント・バージョンを使用します。

リスト 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 サーバー上で割り当てられる 1 日当たりの CPU 時間を超過する可能性があります。

まとめ

この記事では、XML 文書の中のデータからオブジェクトを作成し、GAE 開発で利用可能なデータストア上にそのオブジェクトを永続化する方法をいくつか説明しました。最後に説明した、SOAP ベースのクライアントとサーバーを使用する方法では、現在、XML のバルク・データを Java でアップロードする方法を利用できるようになっています。


ダウンロード可能なリソース


関連トピック

  • Eclipse IDE: Eclipse 開発環境のホームページにアクセスしてください。
  • JDO を利用したデータストアの使用: このガイドを参照し、スケーラブルな Web アプリケーションのデータを保存する方法について学んでください。
  • Google App Engine: Google アプリケーションを実現するシステムと同じシステム上での Web アプリケーションの作成方法とホスト方法について、詳しい資料を読んでください。
  • Datastore Java API ガイド: クエリー・エンジンとアトミックなトランザクション機能を備えた、スキーマレスなオブジェクト・データストアのドキュメントを読んでください。
  • データのアップロード: この記事で説明したものとは別の、Java ではないバルク・ローダー・ツールにより、アプリケーションのデータストアからのアップロード、アプリケーションのデータストアへのダウンロードができることを学んでください。
  • W3C による XML の定義: Web を始めとするさまざまな場所で、大規模な電子出版や多種多様なデータの交換を処理する、この単純で柔軟なテキスト・フォーマットについて学んでください。
  • Exposing SOAP Service on GAE: この 3 回連載の記事を読み、Spring を使って GAE 上で SOAP による Web サービスを実装する方法を学んでください。
  • SAX プロジェクトのサイト: XML パーサーによって XML 文書からソフトウェア・アプリケーションに効率的に情報を渡せることを学んでください。
  • Simple を使って XML シリアライズを行う: Java オブジェクトから XML への変換が実に簡単に行えるようになります」(Brian Carey 著、developerWorks、2009年11月): Simple を使って XML 文書を POJO に変換する方法を理解してください。
  • W3C による SOAP の仕様: SOAP について、また SOAP の特質である、表現によるメッセージ構造とメッセージ交換パターンについて学んでください。
  • Issue 5: wsc-gae-16.0.jar throws null pointer exception if no parent directory specified for jar output file (問題 5: JAR 出力ファイルの親ディレクトリーが指定されない場合に wsc-gae-16.0.jar がヌル・ポインター例外をスローする): WSDL ファイルから Web サービスを作成する手順について、既知の問題についての資料を読んでください。
  • W3C による WSDL の定義: WSDL について学んでください。WSDL はネットワーク・サービスを一連のエンドポイントとして記述する XML フォーマットであり、エンドポイントでは文書指向または手続き指向いずれかの情報を含むメッセージを処理します。
  • developerWorks の XML ゾーン: XML の領域でのスキルを磨くためのリソースが豊富に用意されています。
  • IBM XML certification: XML および関連技術において IBM 認定技術者になる方法を参照してください。
  • XML の技術ライブラリー: developerWorks の XML ゾーンには、広範な話題を網羅した技術記事やヒント、チュートリアル、技術標準、IBM Redbooks などが用意されています。また、XML に関するヒントを読んでください。
  • developerWorks on Twitter: 今すぐ Twitter に参加して developerWorks のツイートをフォローしてください。
  • developerWorks podcasts: ソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
  • Google App Engine SDK for Java: Google Plugin for Eclipse を含めてダウンロードし、標準的な Java 技術を使用して Web アプリケーションの作成を開始してください。そしてそれらの Web アプリケーションを Google のスケーラブルなインフラの上で実行してください。
  • Spring フレームワークのホームページ: Spring プラットフォームをダウンロードしてエンタープライズ Java アプリケーションを作成、実行してください。
  • Force.com Web Service Connector for GAE: Google のストリーミング・パーサーを使って実装されたハイパフォーマンスの Web サービス・クライアント・スタックをダウンロードしてください。
  • wsc-gae-16_0.jar: Force.com の WSC (Web Service Connector) の GAE バージョンを Force.com からダウンロードしてください。
  • soapUI ツール: オープンソースの機能テストツール、soapUI をダウンロードしてください。soapUI は主に Web サービスのテストに使われています。
  • wsc-17_0.jar: WSC JAR ファイルのスタンドアロン版をダウンロードしてください。
  • IBM 製品の評価版: IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML
ArticleID=550604
ArticleTitle=Google App Engine に XML データをインポートする
publish-date=09072010