TopLink EssentialsをTomcatで動かす

http://d.hatena.ne.jp/da-yoshi/20060528あたりの続きで、TopLinkを使って色々動かしてみたいと思います。
前回、ローカル環境でSeasar2JTAと連携して動かし、javaagentを使ってLAZYロードが出来るところまで確認しました。とはいっても、O/Rマッピングを最も使う環境はやはりWebアプリケーションだと思うので、今回はTomcat上で動かしてみたいと思います。
前回の調査で、TopLinkには少なくとも2つの不具合があるらしいことがわかってます。一つは、別のPersistenceProviderをpersistence.xmlで指定していても無理やりTopLinkでEntityManagerFactoryを作成しようとしてしまい、途中でぬるぽで落ちてしまうということ。もう一つは、EntityManagerを作成したときにトランザクションとの紐付けが行われていないので、コンテナ側でjoinTransactionを呼んであげないといけないということ。GlassFishのV2build09をダウンロードして、これらの不具合を確認してみましたが、現在もこれらは直ってないみたいです。joinTransaction問題については、独自のPersistenceProviderを作成出来ればすぐに解決できるのですが、一番目の不具合のせいでTopLink標準のPersistenceProviderを使わなければいけません。とりあえずどうやって動かそうかと思ったのですが・・・S2のPersistenceUnitManagerを継承して、EntityManager作成時にjoinTransactionを行うように修正し、それをjpa.diconに登録してみることにしました。こうすればTxScopedEntityManagerProxyは標準のクラスが使えるので、S2TigerのEJB3関連機能にも対応出来ます。
ここでもう一つ問題発生。TopLinkはDataSouceを取得するときにInitialContextを使用するのですが、オプションのHashtableを受け取ってくれないので、InitailContextFactoryを設定することが出来ません。Tomcat上ではjndi.propertiesも効かないみたいなので、ローカル環境と同じ方法では動きません。TomcatのDataSource取得関連を色々調べていたところ、どうやらObjectFactoryをserver.xmlに設定できるみたいなので、これを利用してみたいと思います。まずは、DataSource取得用のObjectFactory。

public class DataSourceFactory implements ObjectFactory {

	public Object getObjectInstance(Object obj, Name name, Context nameCtx,
			Hashtable<?, ?> environment) throws Exception {
		return new DelegateDataSource(name.toString());
	}

}

このクラスが返すDataSourceクラス

public class DelegateDataSource implements DataSource {
	
	private String name;

	public DelegateDataSource(String name) {
		this.name = name;
	}

	public Connection getConnection() throws SQLException {
		return getParent().getConnection();
	}

	public Connection getConnection(String username, String password)
			throws SQLException {
		return getParent().getConnection(username, password);
	}

	public PrintWriter getLogWriter() throws SQLException {
		return getParent().getLogWriter();
	}

	public int getLoginTimeout() throws SQLException {
		return getParent().getLoginTimeout();
	}

	public void setLogWriter(PrintWriter out) throws SQLException {
		getParent().setLogWriter(out);
	}

	public void setLoginTimeout(int seconds) throws SQLException {
		getParent().setLoginTimeout(seconds);

	}
	
	private DataSource getParent() {
		S2Container container = SingletonS2ContainerFactory.getContainer();
		return (DataSource) container.getComponent(name);
		
	}

}

これをserver.xmlに登録

<Resource name="jdbc.DataSource" auth="Container"
      		type="javax.sql.DataSource"
      		factory="test.toplink.jpa.DataSourceFactory"/>

めんどくさいですが、これで何とかTopLinkにS2のDataSourceを渡せるようになりました。
さて、この環境で何か動かしてみたいと思います。現在、平行してAjaxを勉強しているので、今回はAjaxのクライアントから顧客情報のCRUDを行う処理を作ってみました。まずはhtml

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="/TestWeb/test.js"></script>
<title>Insert title here</title>
</head>
<body>
<form id="form1" action="">
ID:<input type="text" name="id" id="customerId" onchange="getCustomer()"><br>
NAME:<input type="text" name="name" id="name"><br>
ADDRESS:<input type="text" name="address" id="address"><br>
PHONE:<input type="text" name="phone" id="phone"><br>
MAIL:<input type="text" name="mail" id="mail"><br>
VERSION:<input type="text" name="version" id="version"><br>
<input type="button" value="登録" id="post" onclick="insertCustomer()">
<input type="button" value="更新" id="put" onclick="updateCustomer()">
<input type="button" value="削除" id="delete" onclick="deleteCustomer()">
</form>
</body>
</html>

続いてJavaScript

function getXmlHttpRequest() {
	if(window.XMLHttpRequest) {
		return new XMLHttpRequest();
	} else if(window.ActiveXObject) {
		return new ActiveXObject("Msxml2.XMLHTTP");
	} else {
		return null;
	}
}

function getCustomer() {
	var id = document.getElementById("customerId").value;
	if (id == "") {
		document.getElementById("post").style.visibility = "visible";
		document.getElementById("put").style.visibility = "hidden";
		document.getElementById("delete").style.visibility = "hidden";
	} else {
		ajax = getXmlHttpRequest();
		document.getElementById("post").style.visibility = "hidden";
		document.getElementById("put").style.visibility = "visible";
		document.getElementById("delete").style.visibility = "visible";
		ajax.onreadystatechange = returnGetCustomer;
		ajax.open("GET", "/TestWeb/Customer/" + id, true);
		ajax.send(null);
	}
	
}

function insertCustomer() {
	ajax = getXmlHttpRequest();
	ajax.onreadystatechange = returnGetCustomer;
	ajax.open("POST", "/TestWeb/Customer/", true);
	ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
	ajax.send(
		"name=" + document.getElementById("name").value
		+ "&address=" + document.getElementById("address").value
		+ "&phone=" + document.getElementById("phone").value
		+ "&mail=" + document.getElementById("mail").value
	);
	
}

function updateCustomer() {
	var id = document.getElementById("customerId").value;
	ajax = getXmlHttpRequest();
	ajax.onreadystatechange = returnGetCustomer;
	ajax.open("POST", "/TestWeb/Customer/" + id, true);
	ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
	ajax.send(
		"name=" + document.getElementById("name").value
		+ "&address=" + document.getElementById("address").value
		+ "&phone=" + document.getElementById("phone").value
		+ "&mail=" + document.getElementById("mail").value
		+ "&version=" + document.getElementById("version").value
	);
}

function deleteCustomer() {
	var id = document.getElementById("customerId").value;
	ajax = getXmlHttpRequest();
	ajax.onreadystatechange = returnDeleteCustomer;
	ajax.open("DELETE", "/TestWeb/Customer/" + id, true);
	ajax.send(null);
}

function returnGetCustomer() {
	if (ajax.readyState == 4) {
		var value = ajax.responseText;
		var retBean = eval("(" + value + ")");
		document.getElementById("name").value = retBean.name;
		document.getElementById("address").value = retBean.address;
		document.getElementById("phone").value = retBean.phone;
		document.getElementById("mail").value = retBean.mail;
		document.getElementById("version").value = retBean.version;
	}
}

function returnDeleteCustomer() {
	if (ajax.readyState == 4) {
		var value = ajax.responseText;
		if (value == "OK") {
			document.getElementById("name").value = "";
			document.getElementById("address").value = "";
			document.getElementById("phone").value = "";
			document.getElementById("mail").value = "";
			document.getElementById("version").value = "";
		}
	}
}

var ajax;

RESTもどきみたいなサンプルにしようと思って、PUTやDELETEを使ってみたのですが、どうやらServletのdoPutは、POSTと同じ方法ではパラメータを受け取れないみたいですね。ここら辺は弱いので(汗)、取り敢えずCREATEもUPDATEもPOSTで済ませてしまいました。
さて、ここまでがクライアント。サーバはとりあえずサーブレットからStatelessSessionBeanを呼ぶ設定にしてみました。とはいっても動かしてるTomcatは5.5.17なのでServlet2.5環境ではないので、地道にInitailContextから呼び出します(苦笑)。
まずはweb.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
	<display-name>TestWeb</display-name>
	
	<servlet>
        <servlet-name>s2servlet</servlet-name>
        <servlet-class>org.seasar.framework.container.servlet.S2ContainerServlet</servlet-class>
        <init-param>
        	<param-name>configPath</param-name>
        	<param-value>app.dicon</param-value>
    	</init-param>
    	<init-param>
        	<param-name>debug</param-name>
        	<param-value>false</param-value>
    	</init-param>
    	<load-on-startup>1</load-on-startup>
    </servlet>
    
	<servlet>
		<description></description>
		<display-name>CustomerServlet</display-name>
		<servlet-name>CustomerServlet</servlet-name>
		<servlet-class>test.servlet.CustomerServlet</servlet-class>
	</servlet>

    <servlet-mapping>
        <servlet-name>s2servlet</servlet-name>
        <url-pattern>/s2servlet</url-pattern>
    </servlet-mapping>
	
	<servlet-mapping>
		<servlet-name>CustomerServlet</servlet-name>
		<url-pattern>/Customer/*</url-pattern>
	</servlet-mapping>
	
	<resource-ref>
      <description>DB Connection</description>
      <res-ref-name>jdbc.DataSource</res-ref-name>
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
  </resource-ref>
</web-app>

続いてCustomerServlet。Servlet直接作るなんてかなり久しぶり(汗)・・・

package test.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.seasar.extension.j2ee.JndiContextFactory;

import test.ejb.CustomerBean;
import test.entity.Customer;

/**
 * Servlet implementation class for Servlet: TestServlet
 *
 */
 public class CustomerServlet extends HttpServlet {
    /**
	 * 
	 */
	private static final long serialVersionUID = -3333548967610996391L;

	private Context context;
	@Override
	public void init() throws ServletException {
		super.init();
		try {
			Properties prop = new Properties();
			prop.setProperty(Context.INITIAL_CONTEXT_FACTORY, JndiContextFactory.class.getName());
			context = new InitialContext(prop);
		} catch (NamingException e) {
			throw new ServletException(e);
		}
	}
	

	@Override
	public void destroy() {
		super.destroy();
		context = null;
	}


	/* (non-Java-doc)
	 * @see javax.servlet.http.HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String id = getId(request);
		CustomerBean cb = getCustomerBean();
		Customer c = cb.getCustomer(Long.valueOf(id));
		beanToJson(c, response);
	}


	/* (non-Java-doc)
	 * @see javax.servlet.http.HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String id = getEmptyIfNull(getId(request));
		request.setCharacterEncoding("UTF-8");
		Customer c = new Customer();
		c.setName(request.getParameter("name"));
		c.setAddress(request.getParameter("address"));
		c.setPhone(request.getParameter("phone"));
		c.setMail(request.getParameter("mail"));
		CustomerBean cb = getCustomerBean();
		if ("".equals(id)) {
			c = cb.insertCustomer(c);
		} else {
			c.setId(Long.valueOf(id));
			c.setVersion(Long.valueOf(request.getParameter("version")));
			c = cb.updateCustomer(c);
		}
		beanToJson(c, response);
	}

	@Override
	protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String id = getId(request);
		CustomerBean cb = getCustomerBean();
		cb.deleteCustomer(Long.valueOf(id));
		PrintWriter out = response.getWriter();
		out.print("OK");
	}

//	@Override
//	protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//		request.setCharacterEncoding("UTF-8");
//		Customer c = new Customer();
//		c.setId(Long.valueOf(getId(request)));
//		System.out.println("id:" + c.getId());
//		c.setName(request.getParameter("name"));
//		System.out.println("name:" + c.getName());
//		c.setAddress(request.getParameter("address"));
//		System.out.println("address:" + c.getAddress());
//		c.setPhone(request.getParameter("phone"));
//		System.out.println("phone:" + c.getPhone());
//		c.setMail(request.getParameter("mail"));
//		System.out.println("mail:" + c.getMail());
//		String v = request.getParameter("version");
//		System.out.println("version:" + v);
//		c.setVersion(Long.valueOf(request.getParameter("version")));
//		CustomerBean cb = getCustomerBean();
//		c = cb.updateCustomer(c);
//		beanToJson(c, response);
//	}
	
	private CustomerBean getCustomerBean() throws ServletException {
//		return (CustomerBean) SingletonS2ContainerFactory.getContainer().getComponent(CustomerBean.class);
		try {
			CustomerBean cb = (CustomerBean) context.lookup("CustomerBean");
			return cb;
		} catch (NamingException e) {
			throw new ServletException(e);
		}
	}

	private String getId(HttpServletRequest request) {
		String contextPath = request.getContextPath();
		System.out.println("contextPath:" + contextPath);
		String servletPath = request.getServletPath();
		System.out.println("servletPath:" + servletPath);
		String uri = request.getRequestURI();
		System.out.println("uri:" + uri);
		String url = request.getRequestURL().toString();
		System.out.println("url:" + url);
		String id = uri.substring(contextPath.length() + servletPath.length() + "/".length());
		System.out.println("id:" + id);
		return id;
	}

	private void beanToJson(Customer c, HttpServletResponse response) throws IOException {
		response.setContentType("text/plain;charset=UTF-8");
		PrintWriter out = response.getWriter();
		out.print("{");
		out.print("\"id\":\"" + getEmptyIfNull(c.getId()) + "\"");
		out.print(",");
		out.print("\"name\":\"" + getEmptyIfNull(c.getName()) + "\"");
		out.print(",");
		out.print("\"address\":\"" + getEmptyIfNull(c.getAddress()) + "\"");
		out.print(",");
		out.print("\"phone\":\"" + getEmptyIfNull(c.getPhone()) + "\"");
		out.print(",");
		out.print("\"mail\":\"" + getEmptyIfNull(c.getMail()) + "\"");
		out.print(",");
		out.print("\"version\":\"" + getEmptyIfNull(c.getVersion()) + "\"");
		out.print("}");
	}  	
	
	private String getEmptyIfNull(Object str) {
		return str == null ? "" : str.toString();
	}
	
}

RESTもどきにしようとしたばっかりにこんなグダグダしたソースに・・・orz。普通にStrutsとかで作っときゃよかった・・・
続いて、CustomerBean。これは一応StatelessSessionBeanにしてみました。

package test.ejb;

import java.util.List;

import test.entity.Customer;

public interface CustomerBean {

	Customer insertCustomer(Customer customer);
	
	Customer updateCustomer(Customer customer);
	
	void deleteCustomer(Long id);
	
	Customer getCustomer(Long id);
	
	List<Customer> getCustomerList();
}
package test.ejb;

import java.util.List;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import test.entity.Customer;

@Stateless(name = "CustomerBean")
public class CustomerBeanImpl implements CustomerBean {
	
	@PersistenceContext(unitName = "em")
	private EntityManager em;

	public void setEm(EntityManager em) {
		this.em = em;
	}

	public void deleteCustomer(Long id) {
		em.createQuery(
			"DELETE FROM"
				+ " Customer c"
			+ " WHERE"
				+ " c.id = :id")
			.setParameter("id", id)
			.executeUpdate();
	}

	public Customer getCustomer(Long id) {
		return em.find(Customer.class, id);
	}

	@SuppressWarnings("unchecked")
	public List<Customer> getCustomerList() {
		return (List<Customer>) em.createQuery(
			"SELECT"
				+ " c"
			+ " FROM"
				+ " Customer c"
			+ " ORDER BY"
				+ " c.id")
			.getResultList();
	}

	public Customer insertCustomer(Customer customer) {
		em.persist(customer);
		return customer;
	}

	public Customer updateCustomer(Customer customer) {
		return em.merge(customer);
	}

}

ここで使っているEntityクラスは以下の通り

package test.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Version;

@Entity
public class Customer {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customerIdGenerator")
	@SequenceGenerator(name = "customerIdGenerator", sequenceName = "CUSTOMER_SEQ", allocationSize = 1)
	private Long id;
	
	private String name;
	
	private String address;
	
	private String phone;
	
	private String mail;
	
	@Version
	private Long version;

	public String getAddress() {
		return address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getMail() {
		return mail;
	}

	public void setMail(String mail) {
		this.mail = mail;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getPhone() {
		return phone;
	}

	public void setPhone(String phone) {
		this.phone = phone;
	}

	public Long getVersion() {
		return version;
	}

	public void setVersion(Long version) {
		this.version = version;
	}

}

作成したテーブルは

CREATE TABLE CUSTOMER(
	ID	NUMBER(18,0)	PRIMARY KEY
	,NAME VARCHAR2(50)
	,ADDRESS VARCHAR2(100)
	,PHONE	VARCHAR2(50)
	,MAIL	VARCHAR2(50)
	,VERSION	NUMBER(18,0)	NOT NULL
)

作ったクラスをS2に登録するdiconファイル。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<include path="javaee5.dicon"/>
	<include path="s2hibernate-jpa.dicon"/>
	
	<component class="test.ejb.CustomerBeanImpl"/>
</components>

s2hibernate-jpa.diconの中にはTopLinkの構成があります。名前を変えてないのは、S2EJB3Testがこのdiconファイル名を自動的に登録するからです。ただ・・・もしかして、このファイル無くてもEJB3アノテーションを使っていたら、JPA関連って自動的に定義されるのかな?
EJB自体はコンポーネント登録しないといけないみたいですが、アスペクト等の設定は、@Statelessをつけてるので自動的に設定されてました。
最後に、persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
	http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
	version="1.0">
	<persistence-unit name="em" transaction-type="JTA">
		<jta-data-source>java:/comp/env/jdbc.DataSource</jta-data-source>
		<exclude-unlisted-classes>false</exclude-unlisted-classes>
		<properties>
			<property name="toplink.target-server" value="test.toplink.jpa.S2ServerPlatform"/>
			<property name="toplink.logging.level" value="FINE"/>
 		</properties>
	</persistence-unit>
</persistence>

S2ServerPlatformは前回作ったもの。TomcatのJNDIを使うので、DataSourceの名前が変わってます。
とりあえずこんな構成で、Tomcat上で簡単なAjaxもどきアプリを動かすことができました。Validationとかデータ存在チェックとか入れてないのでかなりグダグダですが、まぁとりあえず今回は動かすのが目的だったので良しとしましょう(苦笑)