Resteasy + Silverlight2 その2

前回はサーバサイドがJBoss5 + Resteasy、クライアントがSilverlight2で単純な一覧取得の例を作ってみました。今回は、加えて登録・更新・削除機能を試してみようと思います。
・・・しかし、ここで問題発生。どうやらSilverlight2(RC0)のHttpWebRequestは、HTTPのPUTとDELETEに対応していないみたいです。・・・ってこれじゃREST対応とは言えないんじゃないの? せっかくJAX-RSとの組合せに使えるかと期待していたのですが・・・
まぁしかしここでやめてしまうのも勿体無いので、RailsPrototype.jsがやっている「RESTに見せかけたGET・POST通信」で急場を凌ごうと思います。SilverlightもREST対応を謳うのであれば、出来るだけ早くこの2メソッドに対応してもらいたいものです・・・
気を取り直して、サーバサイドから。前回作成したJAX-RSのクラスに登録・更新・削除用メソッドを追加します。まずはインターフェイスから

package resteasy.resource;
import javax.ejb.Local;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import resteasy.entity.User;
import resteasy.entity.Users;

@Local
@Path("/users")
public interface UsersResourceLocal {
	@GET
	@Produces(MediaType.APPLICATION_XML)
	Users getUsers();
	
	@POST
	@Consumes(MediaType.APPLICATION_XML)
	void insertUser(User user);
	
	@POST
	@Path("/put/{id}")
	@Consumes(MediaType.APPLICATION_XML)
	void updateUserPost(@PathParam("id") Integer id, User user);
	
	@PUT
	@Path("/{id}")
	@Consumes(MediaType.APPLICATION_XML)
	void updateUser(@PathParam("id") Integer id, User user);
	
	@DELETE
	@Path("/{id}")
	void deleteUser(@PathParam("id") Integer id);
	
	@POST
	@Path("/delete/{id}")
	void deleteUserPost(@PathParam("id") Integer id);
}

登録・更新時は、クライアントからXMLデータを取得し、JAXBによりUserクラスに変換します。PUTに擬似対応するPOSTメソッドとしてupdateUserPostを、DELETEに擬似対応するPOSTメソッドとして、deleteUserPostをそれぞれ定義しました。@PathParamを付与したパラメータにIDが渡されるのですが、この引数は自動的に指定した型に変換してくれるようです。
続いて実装クラス。

package resteasy.resource;

import java.util.List;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;

import resteasy.entity.User;
import resteasy.entity.Users;

/**
 * Session Bean implementation class UsersResource
 */
@Stateless
public class UsersResource implements UsersResourceLocal {

	@PersistenceContext
	private EntityManager em;
	
	@SuppressWarnings("unchecked")
	@Override
	public Users getUsers() {
		List<User> users = (List<User>) em.createQuery("SELECT u FROM User u ORDER BY u.id")
			.getResultList();
		return new Users(users);
	}

	@Override
	public void insertUser(User user) {
		em.persist(user);
	}

	@Override
	public void updateUser(Integer id, User user) {
		if (!id.equals(user.getId())) {
			throw new WebApplicationException(Status.BAD_REQUEST);
		}
		em.merge(user);
	}

	@Override
	public void updateUserPost(Integer id, User user) {
		updateUser(id, user);
	}
	
	@Override
	public void deleteUser(Integer id) {
		User user = em.find(User.class, id);
		em.remove(user);
	}

	@Override
	public void deleteUserPost(Integer id) {
		deleteUser(id);
	}
	
}

前回書いたとおり、JAXBのクラスであるUserクラスはJPAのEntityクラスでもあるので、直接EntityManagerに渡して登録・更新処理が行えます。削除処理に関しては、厳密に考えるならバージョンNoも渡して排他チェックをするべきなんでしょうけど、ここでは省略しました。
サーバサイドはここまで。次はSilverlightの方です。登録・更新・削除をどうするか迷ったのですが、ここは単純に、DataGridで選択した行に対して、各ボタンのイベントで処理を行うようにしました。登録・更新処理については別途データ登録画面を設けて、TabControlで切り替えます。

<UserControl xmlns:basics="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"  xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"  x:Class="SilverlightApplication1.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <basics:TabControl x:Name="Tab">
            <basics:TabItem x:Name="ListTab" Header="一覧">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="240"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <data:DataGrid x:Name="Users" Grid.ColumnSpan="3" Grid.Row="0" Grid.Column="0">
                    </data:DataGrid>
                    <Button x:Name="Insert" Content="追加" Grid.Row="1" Grid.Column="0"/>
                    <Button x:Name="Update" Content="更新" Grid.Row="1" Grid.Column="1"/>
                    <Button x:Name="Delete" Content="削除" Grid.Row="1" Grid.Column="2"/>
                </Grid>
            </basics:TabItem>
            <basics:TabItem x:Name="DetailTab" Header="詳細" Visibility="Collapsed">
                <Grid x:Name="DetailRoot">
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="Id:" Grid.Row="0" Grid.Column="0"/>
                    <TextBlock x:Name="Id" Text="{Binding Id}" Grid.Row="0" Grid.Column="1"/>
                    <TextBlock Text="Code:" Grid.Row="1" Grid.Column="0"/>
                    <TextBox x:Name="Code" Text="{Binding Code, Mode=TwoWay}" Grid.Row="1" Grid.Column="1"/>
                    <TextBlock Text="Password:" Grid.Row="2" Grid.Column="0"/>
                    <TextBox x:Name="Password" Text="{Binding Password, Mode=TwoWay}" Grid.Row="2" Grid.Column="1"/>
                    <TextBlock Text="Name:" Grid.Row="3" Grid.Column="0"/>
                    <TextBox x:Name="UserName" Text="{Binding UserName, Mode=TwoWay}" Grid.Row="3" Grid.Column="1"/>
                    <TextBlock Text="Age:" Grid.Row="4" Grid.Column="0"/>
                    <TextBox x:Name="Age" Text="{Binding Age, Mode=TwoWay}" Grid.Row="4" Grid.Column="1"/>
                    <TextBlock Text="Version:" Grid.Row="5" Grid.Column="0"/>
                    <TextBlock x:Name="Version" Text="{Binding Version}" Grid.Row="5" Grid.Column="1"/>
                    <Button x:Name="DetailUpdate" Content="更新" Grid.Row="6" Grid.Column="0"/>
                </Grid>
            </basics:TabItem>
        </basics:TabControl>
    </Grid>
</UserControl>

一覧TABの方にユーザリストと登録・更新・削除ボタンが表示され、詳細TABの方には入力フォームがあります。入力フォームをまとめるGridのDataContextに、フォームデータがバインドされます。
続いて、C#のクラスによるイベントの記述です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Xml;
using System.Xml.Linq;

namespace SilverlightApplication1
{
    public partial class Page : UserControl
    {
        private static string URI = "http://localhost:8080/resteasyWeb/users";

        public Page()
        {
            InitializeComponent();
            Loaded += new RoutedEventHandler(Page_Loaded);
        }

        void Page_Loaded(object sender, RoutedEventArgs e)
        {
            List_Init();
            Insert.Click += new RoutedEventHandler(Insert_Click);
            Update.Click += new RoutedEventHandler(Update_Click);
            Delete.Click += new RoutedEventHandler(Delete_Click);
            DetailUpdate.Click += new RoutedEventHandler(DetailUpdate_Click);
        }

        private void List_Init()
        {
            WebClient client = new WebClient();
            Uri uri = new Uri(URI);
            client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
            client.DownloadStringAsync(uri);
        }

        void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            XDocument doc = XDocument.Parse(e.Result);
            Users.ItemsSource = from user in doc.Descendants("user")
                                select new User
                                {
                                    Id = (int)user.Element("id"),
                                    Code = (string)user.Element("code"),
                                    Password = (string)user.Element("password"),
                                    UserName = (string)user.Element("name"),
                                    Age = (int)user.Element("age"),
                                    Version = (int)user.Element("version")
                                };
        }

        void Insert_Click(object sender, RoutedEventArgs e)
        {
            DetailTab.Visibility = Visibility;
            Tab.SelectedItem = DetailTab;
            User user = new User();
            DetailRoot.DataContext = user;
        }

        void Update_Click(object sender, RoutedEventArgs e)
        {
            DetailTab.Visibility = Visibility;
            Tab.SelectedItem = DetailTab;
            User user = Users.SelectedItem as User;
            DetailRoot.DataContext = user;
        }

        void Delete_Click(object sender, RoutedEventArgs e)
        {
            User user = Users.SelectedItem as User;
//            String method = "DELETE";
//            Uri uri = new Uri(URI + "/" + user.Id);
            String method = "POST";
            Uri uri = new Uri(URI + "/delete/" + user.Id);
            HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
            request.Method = method;
            request.BeginGetResponse(new AsyncCallback(ResponseHandler), request);

        }

        void ResponseHandler(IAsyncResult asyncResult)
        {
            HttpWebRequest request = asyncResult.AsyncState as HttpWebRequest;
            HttpWebResponse response = request.EndGetResponse(asyncResult) as HttpWebResponse;
            if (response.StatusCode == HttpStatusCode.OK)
            {
                this.Dispatcher.BeginInvoke(() =>
                {
                    List_Init();
                    Tab.SelectedItem = ListTab;
                });
            }
        }

        void DetailUpdate_Click(object sender, RoutedEventArgs e)
        {
            User user = DetailRoot.DataContext as User;
            String method = null;
            Uri uri = null;
            if (user.Id == null)
            {
                method = "POST";
                uri = new Uri(URI);
            }
            else
            {
//                method = "PUT";
//                uri = new Uri(URI + "/" + user.Id);
                method = "POST";
                uri = new Uri(URI + "/put/" + user.Id);
            }
            HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
            request.Method = method;
            request.ContentType = "application/xml";
            request.BeginGetRequestStream(r1 =>
            {

                using (Stream stream = request.EndGetRequestStream(r1))
                {
                    using (XmlWriter writer = XmlWriter.Create(stream))
                    {
                        writer.WriteStartElement("user");
                        if (user.Id != null)
                        {
                            writer.WriteStartElement("id");
                            writer.WriteString(user.Id.ToString());
                            writer.WriteEndElement();
                        }
                        writer.WriteStartElement("code");
                        writer.WriteString(user.Code);
                        writer.WriteEndElement();
                        writer.WriteStartElement("password");
                        writer.WriteString(user.Password);
                        writer.WriteEndElement();
                        writer.WriteStartElement("name");
                        writer.WriteString(user.UserName);
                        writer.WriteEndElement();
                        writer.WriteStartElement("age");
                        writer.WriteString(user.Age.ToString());
                        writer.WriteEndElement();
                        if (user.Version != null)
                        {
                            writer.WriteStartElement("version");
                            writer.WriteString(user.Version.ToString());
                            writer.WriteEndElement();
                        }
                        writer.WriteEndElement();

                        writer.Flush();

                    }

                }
                request.BeginGetResponse(r2 =>
                {
                    HttpWebResponse response = request.EndGetResponse(r2) as HttpWebResponse;
                    if (response.StatusCode == HttpStatusCode.OK)
                    {
                        this.Dispatcher.BeginInvoke(() =>
                        {
                            List_Init();
                            Tab.SelectedItem = ListTab;
                        });
                    }
                }, null);
            }, null);

        }

    }
}

GETとPOSTしか使っていないので、WebClientでもいいんでしょうけど、せっかくREST的に考えていたのでWebRequestを使ってみました。クラスオブジェクトからXMLデータの変換がよくわからなかったので、XmlWriterクラスで直接変換してみました。登録時はid、versionの値を空にして最初試してみたのですが、どうやらJAXB側の変換で、値が無いと「0」で定義してしまうようです。これだとJPAの登録処理でエラーになってしまうので、クライアント側でタグそのものを記述しないように修正しています。処理成功時には一覧を更新します。例外発生時の処理は書けていません。WebRequestに関してはよくわかっていないまま書いているので、間違ってるところがあるかもしれません・・・
(追記)後、Silverlight側のUserクラスを若干変更しています。

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverlightApplication1
{
    public class User
    {
        public int? Id { get; set; }
        public string Code { get; set; }
        public string Password { get; set; }
        public string UserName { get; set; }
        public int Age { get; set; }
        public int? Version { get; set; }
    }
}

これで、前回同様JBossサーバにデプロイして動かしてみました。


デザインは適当ですが(汗)、正常に動作しました。Silverlightにはもっともっと慣れる必要がありますが、少しの学習でも最低限の入力系アプリを組めるのは魅力的ですね。JAX-RSに関しては、やはりJAXBを中心としたデータ←→オブジェクト変換機能が強力です。Javaも.netもXML関連のライブラリが充実しているので、両者を組み合わせる場合はXMLが最適解かもしれません。また、JAXBを使えばJavaクラスからXMLスキーマが作れる筈なので、それを更にC#のライブラリと連携させれば、もう一段単純化したデータのやり取りが可能になるかも・・・
一つ残念なのは、前にも書いたとおりSilverlightのREST対応の中途半端さでしょうか。これさえ完璧なら文句無しだったんですけどね・・・