2015年1月11日日曜日

Play!アプリとBoneScriptの通信 - 2.Playアプリ

もうこの本なしでは行きていけない(Play的に)

というわけで、前回の続きです。

  1. 構成
  2. Playアプリ
  3. BoneScript

■機能■

  1. Webページ
    1. ログイン
    2. グラフ表示
  2. Web API
    1. ユーザ登録 / 更新
    2. データ受信
    3. データ返信
こんだけです。ユーザ登録もAPIで済ませてます…まぁ自分用なので。

■githubリポジトリ■

公開しました。TareObjects/ShowSensorsです。

■ダメなコード■

公開してから気づいたのですが、System.outがそのまま残っていますねぇ…お恥ずかしい。一度必要なところはログに書き換えたのですが、余計なことをしてroll backかけた際に巻き添えになり、また書きなおすのも面倒なのでそのままに。

そのうち直しますm(_ _)m。

Calendarを定義しているところでやたらと
calFrom.set(Calendar.MILLISECOND, 0);
ってのが出てきます。不要なところもあるのですが、ミリ秒単位に初期化されていない値が入ってて一致しない、というバグでしばらく時間を潰してしまったので、それ以来入れてます。まさにあつものに懲りてなますを吹くってやつですねorz

■一応、Playプロジェクトを作るところから■

コマンドラインで
$ activator new ShowSensors
を実行し、プロンプトでplay-javaテンプレートを選択します。できたら
cd ShowSensors
activator run
これでアプリに必要なライブラリなどが自動的にダウンロードされ、ビルドされます。メッセージが落ち着いたらブラウザで http://localhost:9000 を表示するとplay!の画面が表示されます。なお、この画面は、conf/routesの

GET     /            controllers.Application.index()
public static Result index() {
return ok(index.render("Your new application is ready."));
}
及びapp/views/index.scala.html, app/views/main.scala.htmlから生成されています。

■Playプロジェクトにソースを追加する■

Play!は「設定より規約」なので、所定のディレクトリにファイルを作れば、あとは勝手にactivatorが認識してくれます。ただ、vimだけで作業している分にはそれでも問題ないのですが、Eclipseで作業する場合にはコマンドライン側でソースを追加した後、一度Eclipseを終了してから
activator eclipse
でeclipse用の設定を更新し、Eclipseを起動してからプロジェクトを右クリック→Refreshをする必要があります。

また、通常はactivator runしたままで作業を続けられるのですが、build.sbtなどappディレクトリの外にあるプロジェクトの構成に関する部分を修正した場合には、一度activatorを終了してから再起動し、activatorのプロンプト上で
update
eclipse
clean
run
とします。cleanは省略しても良いはずですが、一度ハマったことがあってそれ以来念のため実行するようにしています。

■モデル■

今回、persistentにはEbeanを使います。Springにはいい思い出がないのと、せっかくPlay!がCoCなのにXMLでチマチマやってられっか、ってことでw

モデルはデータベース用とWeb API用が必要です。データベース用はUser.javaSensorData.java、Web APIとして返すJsonの定義にSensorDataRest.javaです。

いずれも特別なことはしていませんが、SensorData.javaでMany to Oneリレーションシップを定義する際に通常はpublicでインスタンス定義すればそれでOKなはずですが何故かうまく動いてくれず、ググったところ「play!のバグじゃね? アクセサにしたら動いたよ」とのことで、回避できました。

ソース:
// Did not work
@NotNull
@ManyToOne(fetch = FetchType.EAGER)
public User dataOwner;
// It's work!
@NotNull
@ManyToOne(fetch = FetchType.EAGER)
private User dataOwner;
public User getDataOwner() {
return dataOwner;
}
public void setDataOwner(User inUser) {
dataOwner = inUser;
}

それにしても、こんな風にクエリを書けるのは、ホントに楽です。S2JDBCを更に洗練した感じで、今までEOFのEOQualifier最高!と思っていましたが、宗旨替えします。
public static List<SensorData> findByUserSinceDateForHours(User inOwner,
Calendar inFrom, int inDuration) {
Date from = inFrom.getTime();
Calendar toCalendar = Calendar.getInstance();
toCalendar.setTime(from);
toCalendar.add(Calendar.HOUR_OF_DAY, inDuration);
toCalendar.set(Calendar.MILLISECOND, 0);
Date to = toCalendar.getTime();
return SensorData.find.where().eq("dataOwner.email", inOwner.email)
.ge("logDate", from)
.lt("logDate", to)
.orderBy().asc("logDate")
.findList();
}


■ユーザ登録/更新■

Application.javaのnewUser()、フツーです。
// ユーザ登録 / 更新
// 対応Routes:
// POST /user/new controllers.Application.newUser()
// テスト用curl
// curl --header "Content-type: application/json" --request POST --data '{"email": "mail@foo.com", "password": "secret", "name": "Koichi KURAHASHI"}' http://localhost:9000/user/new
public static Result newUser() {
JsonNode json = request().body().asJson();
if (json == null) {
return badRequest("Expecting Json data");
} else {
String strEmail = json.findPath("email").textValue();
String strPassword = json.findPath("password").textValue();
String strName = json.findPath("name").textValue();
if (strEmail == null || strPassword == null || strName == null) {
return badRequest("Missing parameter");
} else {
User user = User.findByEmail(strEmail);
if (user == null) {
user = new User(strEmail, strPassword, strName);
user.save();
return ok("received and saved.\n");
} else {
user.password = strPassword;
user.name = strName;
user.update();
return ok("received and updated.\n");
}
}
}
}


■データ受信■

Application.java、receive()。curlをそのままソースに貼っておくと手動テストが楽ですが、こうやってコピペに頼るからいつまで経っても覚えないんですよね…。
// BBBからのJsonを受け取る
// 対応routes:
// POST /receive controllers.Application.receive()
// テスト用curl:
// curl --header "Content-type: application/json" --request POST --data '{"date": "2015-01-07 01:30", "count": 12, "email": "mail@foo.com"}' http://localhost:9000/receive
public static Result receive() {
JsonNode json = request().body().asJson();
if (json == null) {
return badRequest("Expecting Json data");
} else {
String strDate = json.findPath("date").textValue();
System.out.println("[" + strDate + "]");
Integer pirCount = json.findPath("count").intValue();
String email = json.findPath("email").textValue();
System.out.println("[" + email + "]");
if (strDate == null || pirCount == null || email == null) {
return badRequest("Missing parameter");
} else {
User user = User.findByEmail(email);
if (user == null)
return badRequest("email address not found");
Date date = null;
SensorData sd = null;
try {
date = sdf.parse(strDate);
} catch (ParseException e) {
e.printStackTrace();
return badRequest("bad date format");
}
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.set(Calendar.MILLISECOND, 0);
if (date != null && user != null) {
sd = SensorData.findSameData(user, cal);
if (sd != null) {
sd.logDate = date;
sd.pirCount = pirCount;
sd.update();
} else {
sd = new SensorData(date, pirCount, user);
sd.save();
}
return ok("received and saved.");
} else {
return badRequest("could not create SensorData");
}
}
}
}


■データ返信■

データを返すAPIとして、GET用とPOST用を用意しました。こっちはGET用です。
// 指定されたユーザの24時間分のデータを取得する
// 対応Routes:
// GET /fetchFromHours controllers.Application.fetchFromHoursGet(email:String ?="foo@bar.com")
// テスト用curl:
// curl --header "Content-type: application/json" --request GET http://localhost:9000/fetchFromHours
@Security.Authenticated(Secured.class)
public static Result fetchFromHoursGet(String inEmail) {
User owner = User.findByEmail(inEmail);
System.out.println(inEmail);
Calendar calFrom = Calendar.getInstance();
calFrom.setTime(new Date());
calFrom.add(Calendar.HOUR_OF_DAY, -24);
calFrom.set(Calendar.MILLISECOND, 0);
Integer hours = 24;
List<SensorData> sds = SensorData.findByUserSinceDateForHours(owner, calFrom, hours);
if (sds == null || sds.size() == 0) {
return badRequest("no data found");
}
List<SensorDataRest> sdr = new ArrayList<SensorDataRest>();
for (SensorData sd : sds) {
sdr.add(new SensorDataRest(sd));
}
return ok(Json.toJson(sdr));
}


■認証■

人生で大事なことはタイミングにC調に無責任、と歌ったのは今は亡き植木等師匠ですが、Playでの認証機能に最低限必要なのは、ログインのためのフォームAuthenticatorの実装、あとは認証の必要なAPIに@Security.Authenticated(Secured.class)アノテーションを付けてやるだけです。

詳細についてはこちら(「認証機能の追加」)…というか、ほぼここからコピペして使ってます。

■データ表示■

Playとしては、Application.javaで単にgraph.scala.htmlを返しているだけです。
// 認証付きグラフページを返す
// 対応Routes:
// GET /graph controllers.Application.graph()
@Security.Authenticated(Secured.class)
public static Result graph() {
return ok(graph.render("dummy"));
}

で、そのgraph.scala.htmlではD3.jsを使って、PlayアプリのWeb API(上記「データ返信」のところ)からデータを読み込み、グラフを描画しています。以下はデータを読み込んで15分ごとの平均値を求める処理です。平均値なんてサーバ側で求めたほうが良いと思うのですが、動かしてみたら思っていた以上に速かったのでそのままにしていますw なお、念のためですが、これだけでグラフが出るわけでなく、この後に縦軸横軸などの設定が続きます。
d3.json("./fetchFromHours", function(data) {
/* Read CSV file: first row => time, count */
var maxval = 0,
sampsize = 0;
var label_array = new Array(),
data1 = new Array();
sampsize = data.length;
var ymdhmFormat = d3.time.format("%Y-%m-%d %H:%M");
var nDatas = 0;
/*
// raw
for (var i=0; i < sampsize; i++) {
label_array[i] = ymdhmFormat.parse(data[i].strDate);
var c = data[i].pirCount;
var tStr = data[i].strDate.slice(11);
data1[nDatas++] = { time: tStr, count: c};
}
*/
// average
var interval = 15;
var sum = 0;
for (var i=0; i < sampsize; i++) {
label_array[i] = ymdhmFormat.parse(data[i].strDate);
var c = parseFloat(data[i].pirCount);
sum += c;
if ((i % interval) == interval-1) {
sum = sum / interval;
if (sum > maxval) maxval = sum;
var tStr = data[i-interval+1].strDate.slice(11);
data1[nDatas++] = { time: tStr, count: sum};
sum = 0;
}
}
x.domain(data1.map(function(d) {return d.time;}));
y.domain([0, d3.max(data1, function(d) { return d.count; })]);

■次回は■

いやー、ほんとにサンプルを切り貼りしただけで何にもしていないのですが、でも1つにまとめて動かすのにホント苦労しましたわ。

さて、次はBoneScriptを公開します(しました)。明日の予定です…三連休で良かった。

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。