GOOS一人読書会 第3回 AuctionSniperEndToEndTest.sniperJoinsAuctionUntilAuctionCloses() - 3

前回終了時のTODOリストは以下の通りです。


TODOリスト

商品ひとつ: 参加し、入札せずに落札に失敗する
 ApplicationRunnerを定義する

  AuctionSniperDriverを定義する
  Mainを定義し、自動でログインできるようにする

 FakeAuctionServerを定義する


商品ひとつ: 参加し、入札し、落札に失敗する
商品ひとつ: 参加し、入札し、落札する
商品ひとつ: 価格の詳細を表示する
複数の商品を扱う
GUIから新しい商品を追加する
指値で入札をやめる

ということでFakeAuctionServerを定義していきたいわけです。

FakeAuctionServerは
- 商品を売り出す(startSellingItem)
- スナイパーからの参加要求を受け取る(hasReceivedJoinRequestFromSniper)
- オークションの終了をスナイパーに伝える(announceClosed)
ができなければいけません。
サーバとスナイパーとの間のやりとりは83ページにのっているようなフォーマットの文字列で行われます。

ひとつの"商品"は、サーバ上の1ユーザとして表現されています。
そして"商品を売り出す"は、"商品ユーザがログインする"ことで表現します。

"スナイパーからの参加要求を受け取る"は、スナイパーユーザから商品ユーザへ決まった形式のメッセージを送信すること、
"オークションの終了をスナイパーに伝える"は、商品ユーザからスナイパーユーザへ決まった形式のメッセージを送信すること、で表現します。

従って、
- サーバにログインする
- 相手ユーザを指定してメッセージを送信する
- メッセージを受信した際に行う処理を指定する
の3つの事ができればよさそうです。

Smackのドキュメントは
http://www.igniterealtime.org/builds/smack/docs/latest/documentation/
にあります。

これによると、
- サーバと接続するためには、サーバのホスト名を指定しXMPPConnectionオブジェクトを作成し、connect()メソッドを呼び出す
- 接続したサーバにログインするためには、接続したXMPPConnectionオブジェクトのlogin()メソッドを使用する。
- ユーザ間のメッセージのやりとりはChatオブジェクトを介して行われる。
- メッセージの送信は、ChatオブジェクトのsendMessageメソッドを使用する。
- メッセージ受信時の処理は、Chatオブジェクト作成時にMessageListenerオブジェクトを渡すことで設定する。

このくらい分かっていればとりあえずはよさそうです。

ということでFakeAuctionServerを写経していきましょう

package test.endtoend.auctionsniper;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ChatManagerListener;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;

import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

public class FakeAuctionServer {
	private final SingleMessageListener messageListener = new SingleMessageListener();
	public static final String ITEM_ID_AS_LOGIN = "auction-%s";
	public static final String AUCTION_RESOURCE = "Auction";
	public static final String XMPP_HOSTNAME = "localhost";
	private static final String AUCTION_PASSWORD = "auction";
	
	private final String itemId;
	private final XMPPConnection connection;
	private Chat currentChat;
	
	public FakeAuctionServer(String itemId) {
		this.itemId = itemId;
                // XMPP_HOSTNAMEで指定されるサーバとの接続を監理するオブジェクト
		this.connection = new XMPPConnection(XMPP_HOSTNAME); 
	}
	
	public void startSellingItem() throws XMPPException {
		connection.connect(); // サーバに接続する

                // 商品ユーザとしてログインする
		connection.login(String.format(ITEM_ID_AS_LOGIN, itemId), AUCTION_PASSWORD, AUCTION_RESOURCE);

                // メッセージが到着したときの処理を指定するために、
                // Chatが作成された際のイベントリスナで、メッセージ到着時の
                // イベントを指定するためのメッセージリスナを登録する
                // 今回はメッセージを容量1のキューに追加するだけ
		connection.getChatManager().addChatListener(
				new ChatManagerListener() {
					public void chatCreated(Chat chat, boolean createdLocally) {
						currentChat = chat;
						chat.addMessageListener(messageListener);
					}
				});
	}
	
	public String getItemId() {
		return itemId;
	}
	
        // スナイパーからの参加要求を受け取ったかどうかを確認する
	public void hasReceivedJoinRequestFromSniper() throws InterruptedException {
		messageListener.receivesAMessage();
	}
	
        // スナイパーにオークションの終了を通知する
        // 今回は空のメッセージを送信するだけ
	public void announceClosed() throws XMPPException {
		currentChat.sendMessage(new Message());
	}
	
	public void stop() {
		connection.disconnect();
	}
	
	public class SingleMessageListener implements MessageListener {
                // 普通のQueueは、満タンなのに要素を追加(add)しようとしたり
                // 空なのに要素を取り出そう(remove)したりすると即座に例外を投げる
                // BlockingQueueは、使えるまで待って追加/取り出しするためのメソッドも用意されている。
                // タイムアウトを指定して、その時間内に要素を取り出せなかったらnullを返す
                // poll()を使用したいからBlockingQueueを使用しているみたい
		private final ArrayBlockingQueue<Message> messages =
				new ArrayBlockingQueue<Message>(1);
		
                // メッセージが到着したら、容量1のキューに追加する
		public void processMessage(Chat chat, Message message) {
			messages.add(message);
		}
		
                // 何らかのメッセージが到着した事を確認する
		public void receivesAMessage() throws InterruptedException {
			assertThat("Message", messages.poll(5, TimeUnit.SECONDS), is(notNullValue()));
		}
	}
}

ソース自体は理解できました。

問題なのは、仮実装で済ませちゃってる部分があるということです。
"あとでやるよー"って事を忘れちゃったら元も子もないので忘れないようにメモしておきます。


TODOリスト

商品ひとつ: 参加し、入札せずに落札に失敗する
 ApplicationRunnerを定義する

  AuctionSniperDriverを定義する
  Mainを定義し、自動でログインできるようにする

 FakeAuctionServerを定義する


  (仮実装)"スナイパーからの参加要求を受け取ったかどうか"の判定で空のメッセージでもOKとした
  (仮実装)"スナイパーへのオークション終了通知"が空のメッセージ

商品ひとつ: 参加し、入札し、落札に失敗する
商品ひとつ: 参加し、入札し、落札する
商品ひとつ: 価格の詳細を表示する
複数の商品を扱う
GUIから新しい商品を追加する
指値で入札をやめる

ここまではあくまでも"テストコード"を書いてきました。

次回からようやく本番コードを書き始めていきます。