Status Code 303 - See Other

サーバサイド、iOS・アンドロイドアプリ、インフラレベルの話まで幅広くやってます。情報の誤りや指摘・意見などは自由にどうぞ。

「プラス・マイナス・ゼロ」問題

概要

CodeIQ で私の解いてた問題で、ブログネタにできそうなものがあったので記述。

課題内容

codeiq.jp

自然数 n に対して、次の等式を考えます。
    □1□2□3□4…□n = 0
四角の部分には、プラス(+)またはマイナス(-)の記号が入ります。
等式を成立させるような記号の入れ方を考えましょう。

例えば、n = 7 のとき、次の 8 通りの入れ方が可能です。

+1+2-3+4-5-6+7 = 0
+1+2-3-4+5+6-7 = 0
+1-2+3+4-5+6-7 = 0
+1-2-3-4-5+6+7 = 0
-1+2+3+4+5-6-7 = 0
-1+2-3-4+5-6+7 = 0
-1-2+3+4-5-6+7 = 0
-1-2+3-4+5+6-7 = 0

自然数 n に対し、等式を成立させる記号の入れ方の数を F(n) と定義します。
例えば F(7) = 8 です。
同様に、F(3) = 2、F(8) = 14 となることが確かめられます。

標準入力から、自然数 n (1 ≦ n ≦ 50)が与えられます。
標準出力に F(n) の値を出力するプログラムを書いてください。

回答(C言語)

まずは、私の回答。

#include<stdio.h>

int main() {
	int i, j, n;
	scanf("%d", &n);

	int allSum = n*(n+1) >> 1;
	if(allSum & 1) {
		printf("0");
		return 0;
	}
	long long int count[allSum+1];
	for(i=0; i<=allSum; i++) count[i]=0L;

	count[0] = 1L;
	for(i=1; i<=n; i++) {
		int prevLength = ((i-1)*i >> 1)+1;
		long long int copy[prevLength];
		for(j=0; j<prevLength; j++) copy[j] = count[j];
		for(j=0; j<prevLength; j++) count[i+j] += copy[j];
	}
	printf("%lld", count[allSum >> 1]);
	return 0;
}

一部シフト演算子とか、ビット演算子とか気持ち悪いものを使っているが、次と同値。(読み辛くて申し訳ない)

  • [式] >> 1 の部分 → [式]/ 2
  • [式] & 1 → [式]が奇数なら1、偶数なら0

言い訳:当初書いたアルゴリズムがひどくて、計算が時間内に終わらず、できる限り計算時間の少なさそうな方法を選んていたから。

説明

単純にやると・・

回答の解説を見て頂ければ分かると思うが、単純に問題を解くと私の環境では n=23 くらいで処理が1秒以内に終了しなくなった。

なお、ここで単純な処理とは各□に+ や - を全組み合わせを試して確認するという方法。
この方法でも正しい結果は論理的に得られるが、アルゴリズムとしては n が 1増加することによって
計算時間が2倍増加してしまうため、アルゴリズムとしては実用的なものではない。

そういうわけで、確認方法を多少工夫する必要があり、その基本方針はまさに回答の「一つ手前の状態を考える」。
私はまず問題を変換してから考えているため、まずそれから説明する。

問題変換

全部のプラスマイナスパターンを確認するのは、面倒なので、まず問題を変換して解きやすくする。

まず、□1□2□3□4…□n = 0 を満たすプラスマイナスの組が合計 0 になる状況では、何が起こるか?

1〜n のプラス集合とマイナス集合の各々の合計値の絶対値は一致する
(例:+1+2-3+4-5-6+7 = 0 → |1+2+4+7| = |-3-5-6|=14
 ↓
各々の集合の絶対値は、1〜n の和{\displaystyle \frac{1}{2}n(n+1)}の半分に相当する
(例:1+2+3+4+5+6+7 = 7*8/2 = 28, 上記絶対値は14)
 ↓
1〜n のうち任意集合の合計値が {\displaystyle \frac{1}{4}n(n+1) } になる組み合わせ数が求めるべき数
 ↓
発生しうる任意集合の和とその組み合わせの数をリスト化したものを作成
 ↓
最後に{\displaystyle \frac{1}{4}n(n+1) }の組み合わせの数を表示

ただし、n=2 のように { S_2 =\displaystyle \frac{1}{2}*2*3 = 3 } が奇数になるものは解が存在しないため除外が必要。
これを下記で行っている。

	int allSum = n*(n+1) >> 1;
	if(allSum & 1) {
		printf("0");
		return 0;
	}

前回の処理結果から次の結果を求める方法

まず、選択した組み合わせの和を配列のインデックスに、その組み合わせ数を配列の値とする。
入力 n から、組み合わせ数を格納する配列長は、0(全て選択しない)〜{ \displaystyle \frac{1}{2}n(n+1) } (全て選択)まで確保すれば良い。

	long long int count[allSum+1];
	for(i=0; i<=allSum; i++) count[i]=0L;

まず、簡単な例を考える。□1 = 0 の場合。発生する部分和とそのカウント数は次の2通り。

[集合の合計値][組み合わせ数]
01
11
ここで、新しく2を追加する。つまり、 □1□2= 0 の場合。
[集合の合計値][これまでの組み合わせ数][2を選択した組み合わせ][組み合わせ数]
01-1
11-1
2-11
3-11

次に3を含めた場合。

[集合の合計値][これまでの組み合わせ数][3を選択した組み合わせ][組み合わせ数]
01-1
11-1
21-1
3112
4-11
5-11
6-11
同様に4を含めた場合。
[集合の合計値][これまでの組み合わせ数][4を選択した組み合わせ][組み合わせ数]
01-1
11-1
21-1
32-2
4112
5112
6112
7-22
8-11
9-11
10-11
これを以下のようにまとめる。
[集合の合計値][選択なし][1選択][2選択][3選択][4選択][組み合わせ数]
01----1
1-1---1
2--1--1
3--11-2
4---112
5---112
6---112
7----22
8----11
9----11
10----11
この表から、これまでの組み合わせ結果が分かれば、その結果と「それを下にn個ずらした組み合わせ数」を加算すれば組み合わせ数が求まる。
あとは、これを実装すれば良い。これを行ってるのが以下。

	count[0] = 1L;
	for(i=1; i<=n; i++) {
		int prevLength = ((i-1)*i >> 1)+1; // 前回には、どの範囲までを対象にすればいいか
		long long int copy[prevLength];
		for(j=0; j<prevLength; j++) copy[j] = count[j]; // 値のコピー
		for(j=0; j<prevLength; j++) count[i+j] += copy[j]; // 追加分足し込み
	}

そして、最終的に欲しい組み合わせ数は、1〜nまでの合計/2 だから。

	printf("%lld", count[allSum >> 1]);

で表示している。

改良点

コピーする必要なし

書いているとき気づいたが、値のコピーを作成するロジックは必要はない。(下記3, 4行目)

	for(i=1; i<=n; i++) {
		int prevLength = ((i-1)*i >> 1)+1; // 前回には、どの範囲までを対象にすればいいか
		long long int copy[prevLength];
		for(j=0; j<prevLength; j++) copy[j] = count[j]; // 値のコピー
		for(j=0; j<prevLength; j++) count[i+j] += copy[j]; // 追加分足し込み
	}

このコピーは、値が2重に足し込まれる問題に対して回避するため、元の値を保持する目的で作成していた。
しかし、この現象はインデックスが小さいものから処理すると発生するが、大きいものから処理すれば起こらない。

	for(i=1; i<=n; i++) {
		int prevLength = (i-1)*i >> 1; // 前回には、どの範囲までを対象にすればいいか
		long long int copy[prevLength];
		for(j=prevLength; j>=0; j--) count[i+j] += copy[j]; // 追加分足し込み
	}

この方がシンプルになる。

配列の初期化
	long long int count[allSum+1];
	for(i=0; i<=allSum; i++) count[i]=0L;
	count[0] = 1L;

配列の初期化は、こんなのでも可能らしく、値が指定されていないと0が自動で入るらしい。
そう思って、下記のように修正・・・。

	long long int count[allSum+1] = {1};

・・と思ったら、これ変数だめなようで、この書き方はサイズを定数で書いて確実にその長さが
存在することを保証しないといけないみたいだ。仕方ないので、n<=50 まで対応 → 50*51/2+1 = 1276 にすることに。

	long long int count[1276] = {1};
おまけ:短いコード

短いコードにするなら、ここまでは小さくできた。

#include<stdio.h>
int main() {
        int n,i=0,j;
        scanf("%d", &n);
        long long a=n*(n+1)/2,c[1276]={1};
        for(;++i<=n;)for(j=a;j>=0;j--)c[i+j]+=c[j];
        printf("%lld",a&1?0:c[a/2]);
}

不要な改行コードとかとって・・

#include<stdio.h>
int main(){int n,i=0,j;scanf("%d",&n);long long a=n*(n+1)/2,c[1276]={1};for(;++i<=n;)for(j=a;j>=0;j--)c[i+j]+=c[j];printf("%lld",a&1?0:c[a/2]);}

163byte。多分、やり方を変更すれば、もうちょっと縮まるかなあ。

Zabbix API を使ってみる

概要

前回の記事で(Raspberry PI に Zabbix Server 導入まで - Status Code 303 - See Other)
作ったZabbix Server に対して、API を発行してみる。今回は、curl で通信してみる。

Zabbix API テスト

BEFORE

Zabbix コンソールから現状のホストグループ構成を調べる。

まず、Zabbix コンソールにアクセスする。ルート管理者でログイン
http://[Raspberry pi プライベート IP]/zabbix/
f:id:kouki_hoshi:20160218001718p:plain

現状ではプライベート環境しか通信できないため、80番ポートでも大丈夫だと思うが、
今後、外部からも通信したいなら、セキュリティ上、SSL/TLS にしたりポート変更したりしなければならない。

そして、[設定]->[ホストグループ] で一覧確認。
f:id:kouki_hoshi:20160218002201p:plain

Zabbix API 実行

サンプルとして、 [設定]->[ホストグループ] に curl コマンドで新しくホストグループを作成する手順を記す。

認証情報を取得する。
curl -X POST -d '{"jsonrpc":"2.0","method":"user.login","id":1,"params":{"user":"userName","password":"xxxxx"}}' -H "Content-Type:application/json-rpc" http://[Raspberry pi プライベート IP]/zabbix/api_jsonrpc.php

やってることを説明すると


curl [options] http://[Raspberry pi プライベート IP]/zabbix/api_jsonrpc.php
Zabbix API を呼び出すコマンドで、 api_jsonrpc.php に対して問い合わせをする
-X POST
HTTP 問い合わせを POST で行う
-d '{"jsonrpc":"2.0","method":"user.login","id":1,"params":{"user":"userName","password":"xxxxx"}}'
問い合わせ時に引き渡すJSONデータ
-H "Content-Type:application/json-rpc"
送信データがJSON-RPC用データであることを送信先に明示する

なお、今回渡したデータは認証のためのデータ。詳細は本家 API 参照のこと。
user.login [Zabbix Documentation 2.2]

こんなデータが返ってくる。

{"jsonrpc":"2.0","result":"3df0b33dc345fe5a29f8b862a4e60bcc","id":1}

そして、「3df0b33dc345fe5a29f8b862a4e60bcc」の部分がトークンと呼ばれる、認証成功時に本人を識別するための情報。
このトークンには期限があり、これが切れると再度取得しなければならない。
それまでであれば、このトークンを API 呼び出しのデータに含めることで、様々な操作が実行できる。

API (hostgroup.create) 実行

test という名のホストグループを新しく作成してみる。API は下記を利用する。
hostgroup.create [Zabbix Documentation 2.2]

curl http://[Raspberry pi プライベート IP]/zabbix/api_jsonrpc.php -H "Content-type: application/json-rpc" -X POST -d '{"jsonrpc":"2.0","method":"hostgroup.create","auth":"3df0b33dc345fe5a29f8b862a4e60bcc","id":1,"params":{"name":"test"}}'

そして、成功すれば、以下のような結果を得る。
これは、ホストグループの作成に成功し、そのホストグループ ID を結果として送られている。

{"jsonrpc":"2.0","result":{"groupids":["11"]},"id":1}

実際に、さっきのページを更新してみると。

f:id:kouki_hoshi:20160218010006p:plain
test という名のホストグループが作られていることが分かる。
同様に、他の API を用いて行えば様々に Zabbix の設定を変更することができる。

ホストグループ以外に作成できるもの

ホストグループは、ホストと呼ばれる統合的な管理オブジェクトをまとめるものである。
だから、これだけでは特に何もできない。そこで、他には何を作成できるか、その一例を記す。


ホスト
様々なオブジェクト(下記)を統合的に管理するオブジェクト
テンプレート
設定情報を保持するオブジェクトで、ホストに設定するとその設定が適用されたホストになる
ユーザマクロ
ホストごとに特有なパラメータを管理するオブジェクト
アイテム
監視データを管理するオブジェクト
アプリケーション
論理的なグループでアイテムをグループ化するためのオブジェクト
トリガー
監視データから異常を検知するオブジェクト
アクション
異常を検知したときに、その動作定義を保持したオブジェクト

他にもあるので、もし興味があれば本家で調べてみるといいだろう。
Zabbix :: The Enterprise-Class Open Source Network Monitoring Solution

Raspberry PI に Zabbix Server 導入まで

概要

Raspberry pi に Zabbix Server を追加した経験談。
自身の失敗をメモしておくとともに、自身の失敗が誰かの救いになれば良い。

・・知識がないとこれほど大変だとは思わなかった。

事の発端

過去に購入したRaspberry Pi

Raspberry Pi 2 Model B (1)

Raspberry Pi 2 Model B (1)

監視カメラを作っている記事を見つけ、実践するつもりだったが、ただ動画キャプチャするだけだと
何か異常が起こっても分からないし、動画全部見るなんて結構面倒くさい。
そこで、状況に応じて、色んな制御を内部で組み込みたいと思っていた。

そして、Zabbix サーバというかなり高機能な運用監視サーバを導入しようと思いたち開始。

立ち上げまでの経緯

MySQL が動かない

MySQL のインストールがうまくいっていない。どうやら MySQL サーバが起動できないらしい。
すでにパッケージはインストールされているが、どうやら起動に毎回失敗している。
mysql のログは /var/log/syslog に出るらしいので、見てみると。

Feb 14 11:40:35 www mysqld: 160214 11:40:35 [ERROR] Can't start server: Bind on TCP/IP port: Cannot assign requested address
Feb 14 11:40:35 www mysqld: 160214 11:40:35 [ERROR] Do you already have another mysqld server running on port: 3306 ?
Feb 14 11:40:35 www mysqld: 160214 11:40:35 [ERROR] Aborting

とにかく接続できないらしい。netstat -t -a -n で調べるが、ポートは問題ない。
どうやら、curl http://127.0.0.1/ping 127.0.0.1 で失敗することから、127.0.0.1 が認識できていないようだ。
とりあえず /etc/mysql/my.cnf にある、bind-address を 127.0.0.1 → 192.168.0.xx に変えてみたら動いた。
確認してみると、過去に SSH の設定したときに、ネットワーク設定を間違ったようだ。
/etc/network/interfaces を確認すると、ループバックアドレス設定が間違っていた。

auto l0
iface lo inet loopback

なぜ今まで気づかなかったのか・・。bind-address も 127.0.0.1 に直して動くことを確認。

apt-get の Zabbix Server のバージョンが古い

無事 zabbix のインストールができたのだが、apt-get で導入した zabbix-server のバージョンが 1.8 。
あまりに古すぎるため、apt-get のミラーサイト変えてみたりして、色々試してみたもののどうしようもなかった。
調べてみると、ソースコードをビルドする方法で、最新バージョンを導入できそうなので (動作するか分からないけど)
今回はとりあえず、apt-get の推奨する動作環境で確認して、後で入れ替えて見ることに。

しかし、Zabbix の初期設定画面がうまくいかなかった。なんか mysql への接続に失敗しているみたい。
理由が分からなかっため、1週間以上放置してしまう。

Raspberry pi だからこそ起こる問題発覚

再開しようと思ってたおり、調査していると現状のままでは色々まずいことを web で発見する。
Zabbix サーバは監視データを逐一記録するため、数多くデータを書き込むが、
SD カード(Raspberry pi記憶媒体)の寿命は書き換え回数ので数千回くらいなんだとか。
SD書き換え回数制限について - その他(ハードウェア) [解決済 - 2015/03/14] | 教えて!goo

ってことは、1分に1回データ書き込んでいたら、書き込み回数が一日 60 * 24 = 1460回 だから、一週間持たないということになる。
ってことで、データは外付け HDD に格納しなければならないと思い購入。

Raspberry Pi のコンパクトさを維持するため、なるべくかさばらないものを選択。
また、対象機種は Windows だけでなく Mac OS X も含まれていると Linux でも動作は問題ないだろうと判断。
容量としては500GB あれば十分と思い、メーカーと値段を考慮して以下に決定。

HDD を認識しない

注文から3日後、HDD が届く。Raspberry pi の USB に HDD を差し込み、起動。
どうやら最初から NTFS でフォーマットされているらしく、フォーマットとかパーティション切らなくていいらしい。
Linux コマンドの分からない自分にとっては調べる手間が省けて嬉しい。

・・・しかし。df コマンド叩くも、50GB くらいの ディスクが出てこない。
マウントしてないから? → fdisk -l コマンドを叩く。→ そもそも外付け HDD の情報がない。

おまけに HDD からカチッカチッと音がする。よくわからないが Raspberry pi は問題なく動作しているように見える。
HDD の初期不良かと思って、 Windows PC に差してみたが動作する。仕方がないので調査。
Raspberry pi 2が外付けHDDを自動認識してくれなかった - Qiita

同じ症状が出てる人結構いるみたいです。
/etc/fstab down't work with RPI2 · Issue #824 · raspberrypi/linux

  • raspi2では起動プロセスが複数コアで並列して行われてる
    • /etc/fstabのパースがHDD認識と同時またはそれ以前に行われてしまう
    • /etc/fstabを読んでる段階ではUUIDとHDDが紐ついていないので、そんなHDDないよってことですっとばされる

各HDDのUUIDが判明するまでマウント作業を待ってもらえばいい。
/boot/cmdline.txtにrootdelay=5を追記。

上記を試してみるも、変化なし。

調べてみると電源が原因の可能性が浮上。私が使っている電源は 1.8A 出力するので、問題ないと思っていたが、
Windows PC に差したときにはカチッカチッという音はしなかったため、この可能性が高いと思い電源ハブを購入。

NTFS じゃだめだった?

電源ハブが届き、apt-get から ntfs-3g パッケージをインストールし、NTFS も読めるようにした。
すると、HDD を認識した。どうやら原因は電源だったようだ。ファイルの一覧も表示できており、動作は問題なさそうだ。

Zabbix サーバでは、監視データなどは基本的に、データベース内に作成するということで、mysql が本来ある場所に
シンボリックリンクを張って、mysql 自体を HDD に移動。
kakakikikekeのブログ: 【CentOS】zabbixのmysqlデータの保存先を変更する方法

service mysqld stop
cd /var/lib
mv mysql [HDD パス]
ln -s [HDD パス]/mysql mysql
service mysqld start

すると。【Can't connect to local MySQL server through socket '/var/lib/mysqld/mysqld.sock'】が出た。
そして、調べてみると、mysqlのファイル/フォルダの所有者が mysql ではなく pi になっている。
なので、chown を使って mysql に変更しようとするが、できない。

エラーが出ていないのに、実行しても所有者が変わらない?・・権限?
念のため、chmod ・・・が、アクセス権自体が変更できない。なんだこれは?

どうやら、ntfs では、マウント後でアクセス権変更したりとかできないらしい。
chmod で変更できません。 - linux 初心者ですlinux mint 14 を... - Yahoo!知恵袋

なので、ディスクを NTFS から ext4 に変更。参考にしたのは以下。
Raspberry Piでファイルサーバ、Part2 外付けハードディスクの導入編 | ものづくりエクスペリメント

UUID 調べて、etc/fstab に設定。

Zabbix も最新バージョンに移行

古いバージョンの zabbix を念のため、全て削除。
Zabbix の本家から最新ソースコードをダウンロードして、適用する。

下記の記事は、Zabbix 2.4.7 環境についてだったが、2.4.7 でも同様の手順で立ち上げられた。
端的にやることとコマンドだけ書かれているため、動かすことを目的としている私にはとても助かった。
raspberry2B/zabbix2.4.4

しかし、動かすことを優先しすぎて設定内容がよくわかってないため、これから設定内容は見直してみようと思う。

SSHJ による暗号化通信

概要

様々な処理をサーバに任せ、その処理結果だけをサーバから取得したい場合がある。

このような通信を行う際、FTPTELNET を用いて行うと
通信が暗号化されないため、その内容を第三者に盗聴されるリスクを生んでしまう。

ここでは、SSH による暗号化通信を Java で実装する方法について記述する。
また、SSH 用のライブラリは多数存在するが、今回は SSHJ(version 0.15.0) を用いる

実装内容

今回は「サーバと簡単な通信が行える」を前提として、次の機能を提供するクラスを作成。

  • 認証 (パスワード/公開鍵(OpenSSHキーファイル)認証)
  • リモート先でのコマンド実行
  • SCP/SFTP を用いたファイルダウンロード
  • SCP/SFTP を用いたファイルアップロード

SCP と SFTP は場合によって使い分ける可能性があると思い、両方使えるようにした。
SCP と SFTP の違いはこちらが参考になる。セキュリティプロトコルマスター(8):軽快なscpか高機能なsftp、sshサーバに向いているのは? (1/2) - @IT

今回は、 SSH 用のライブラリを今後取り替える可能性も考慮して、ロジックにこれらを記述せず
Facade クラス(Facade - GoF デザインパターン - Status Code 303 - See Other) を作成、これらを利用する。

詳細コード

ビルドパス設定

SSHJ

今回の SSH のコアとなるライブラリ

  • sshj-0.15.0.jar
SSHJ を使用する場合に必要なライブラリ

暗号化ライブラリ

  • bcpkix-jdk15on-154.jar
  • bcprov-jdk15on-154.jar
  • ecc-25519-java-1.0.1.jar

圧縮ライブラリ (ダウンロード/アップロードで必要)

  • jzlib-1.1.3.jar

ログ出力ライブラリ

Apache Mina の提供する SSH ライブラリ

  • sshd-core-1.0.0.jar
ボイラープレートコード排除

クラス

SSHJ のFacade クラス (SSHClientFacade.java)

package communicate.ssh;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import net.schmizz.sshj.xfer.FileSystemFile;

public class SSHClientFacade implements AutoCloseable {

	/**
	 * SSH Client
	 */
	SSHClient ssh = new SSHClient();

	/**
	 * Connect server.
	 * 
	 * @param host
	 * @param port
	 * @throws IOException
	 */
	public void connect(String host, int port) throws IOException {
		ssh.useCompression(); // need jzlib
		ssh.addHostKeyVerifier(new PromiscuousVerifier());
		ssh.connect(host, port);
	}

	/**
	 * Add user information with key file.
	 * 
	 * @param user
	 * @param keyFile
	 * @throws TransportException
	 * @throws UserAuthException
	 */
	public void authPublickey(String user, File keyFile) throws UserAuthException, TransportException {
		FileKeyProvider provider = new OpenSSHKeyFile();
		provider.init(keyFile);
		ssh.authPublickey(user, provider);
	}
	
	/**
	 * Add user information with password.
	 * 
	 * @param user
	 * @param password
	 * @throws IOException
	 */
	public void authPassword(String user, String password) throws UserAuthException, TransportException {
		ssh.authPassword(user, password);
	}
	
	/**
	 * Execute Command.
	 * 
	 * @param command
	 * @return SSH Result
	 * @throws IOException
	 */
	public SSHResult execute(String command) throws IOException {
		try (final Session session = ssh.startSession()) {
			Command cmd = session.exec(command);
			cmd.join(30, TimeUnit.SECONDS);
			String output = IOUtils.readFully(cmd.getInputStream()).toString();
			Integer status = cmd.getExitStatus();
			if (status == 0) {
				return new SSHResult(status, output, "");
			}
			return new SSHResult(status, output, IOUtils.readFully(cmd.getErrorStream()).toString());
		}
	}

	/**
	 * Download file.
	 * 
	 * @param src
	 *            Source file path (remote)
	 * @param dest
	 *            Destination directory or file path (local)
	 * @param method
	 *            SCP or SFTP
	 * @throws IOException
	 */
	public void download(String src, File dest, TransferMethod method) throws IOException {
		method.download(ssh, src, dest);
	}

	/**
	 * Upload file.
	 * 
	 * @param src
	 *            Source file path (local)
	 * @param dest
	 *            Destination directory or file path (remote)
	 * @param method
	 * @throws IOException
	 */
	public void upload(File src, String dest, TransferMethod method) throws IOException {
		method.upload(ssh, src, dest);
	}

	/**
	 * Tear down.
	 * 
	 * @throws IOException
	 */
	@Override
	public void close() throws IOException {
		try {
			ssh.close();
		} finally {
			ssh.disconnect();
		}
	}

	/**
	 * Transfer method strategy.
	 *
	 */
	public static enum TransferMethod {
		SCP {
			@Override
			void download(SSHClient ssh, String src, File dest) throws IOException {
				ssh.newSCPFileTransfer().download(src, new FileSystemFile(dest));
			}

			@Override
			void upload(SSHClient ssh, File src, String dest) throws IOException {
				ssh.newSCPFileTransfer().upload(new FileSystemFile(src), dest);
			}
		},
		SFTP {
			@Override
			void download(SSHClient ssh, String src, File dest) throws IOException {
				ssh.newSFTPClient().get(src, new FileSystemFile(dest));
			}

			@Override
			void upload(SSHClient ssh, File src, String dest) throws IOException {
				ssh.newSFTPClient().put(new FileSystemFile(src), dest);
			}
		};

		/**
		 * Download File.
		 * 
		 * @param ssh
		 *            SSHClient
		 * @param src
		 *            Source file(remote)
		 * @param dest
		 *            Destination directory or file path(local)
		 * @throws IOException
		 */
		abstract void download(SSHClient ssh, String src, File dest) throws IOException;

		/**
		 * Upload File.
		 * 
		 * @param ssh
		 *            SSHClient
		 * @param src
		 *            Source file(local)
		 * @param dest
		 *            Destination directory or file path(remote)
		 * @throws IOException
		 */
		abstract void upload(SSHClient ssh, File src, String dest) throws IOException;
	}
}

SSHClient から返る結果を格納するクラス (SSHResult.java)

package communicate.ssh;

import lombok.Value;

@Value
public class SSHResult {
	
	/**
	 * Exit status of command.
	 */
	public int status;
	
	/**
	 * Output.
	 */
	public String output;

	/**
	 * Error.
	 */
	public String error;
}

設定ファイル (ssh.properties)

# Connection Settings
ssh.host = [必須, IPアドレス、ホスト名どちらでも可]
ssh.port = [必須]

# User Authentication
ssh.user = [必須]
ssh.keyfile = [キーファイルのパスを指定、今回はOpenSSH形式のキーファイル対応]
ssh.password = [パスワード、公開鍵認証なら空でも良い]

使い方

  1. ssh.properties を変更する
    • ssh.host, ssh.port, ssh.user は設定必須
    • パスワード認証 → ssh.password 設定
    • 公開鍵認証 → ssh.keyfile 設定 (今回は、OpenSSLキーファイルのみ対応)
  2. ロジックを記述する

(Property クラス → プロパティファイルの使用方法 - Status Code 303 - See Other)

package communicate.ssh;

import java.io.File;
import java.io.IOException;

import communicate.ssh.SSHClientFacade.TransferMethod;
import resources.properties.Property;

public class Main {

	public static void main(String[] args) throws IOException {

		Property property = new Property("ssh");
		try (SSHClientFacade ssh = new SSHClientFacade()) {			
			// 接続
			ssh.connect(property.getString("ssh.host"), property.getInt("ssh.port"));

			// 認証
			ssh.authPublickey(property.getString("ssh.user"), new File(property.getString("ssh.keyfile")));
			// ssh.authPassword(property.getString("ssh.user"), property.getString("ssh.password"));

			// コマンド実行
			SSHResult result = ssh.execute("ls -al > test.txt");
			System.out.println(result);

			// ファイルのダウンロード
			ssh.download("/home/pi/test.txt", new File("./sample"), TransferMethod.SCP); // SCP の場合
			// ssh.download("~/test.txt", new File("./"), TransferMethod.SFTP);  //SFTP の場合

			// ファイルのアップロード
			ssh.upload(new File("./sample"), "/home/pi/", TransferMethod.SCP); // SCP の場合
			// ssh.upload(new File("./test.txt"), "/home/pi/", TransferMethod.SFTP); // STFP の場合
		}
	}	
}

注意点

リモート先のパスは、なるべくフルパスで記述した方が良い。不思議なことに、ヘテムルでは
download メソッドの引数ではホームディレクトリ(~/xxx/yyy) を解釈するが、upload 時は認識できないようで、
下記の例外が発生した。なお、フルパスに変更すると動作した。

Exception in thread "main" net.schmizz.sshj.xfer.scp.SCPRemoteException: Remote SCP command had error: scp: ~/xxx/yyy: No such file or directory

接続方法を様々に変更する拡張

今回は単純化のため、サーバへの接続方法が同じであると仮定して実装した。
しかし、様々なサーバに接続する際に、その接続設定が異なることもあるだろう。
これらは、SSHClient の設定を変更することで実現できる。

接続方法の追加

まず SSHClientFacade クラスに以下を追加する。

	// アクセス方法に関する Strategy
	public static interface AccessStrategy {
		public abstract void execute(SSHClient ssh) throws IOException;
	}
	// connect メソッドを追加する
	public void connect(String host, int port, AccessStrategy method) throws IOException {
		method.execute(ssh);
		ssh.connect(host, port);
	}

そして、ロジックでは次のように使う想定。

	ssh.connect(property.getString("ssh.host"), property.getInt("ssh.port"), new AccessStrategy(){
		@Override
		public void execute(SSHClient ssh) throws IOException {
			ssh.setConnectTimeout(60 * 1000);
			ssh.setTimeout(30 * 60 * 1000);
			ssh.loadKnownHosts();
			ssh.addHostKeyVerifier(new PromiscuousVerifier());
		}				
	});

これで、サーバによって柔軟な設定が可能になり、SSHClient を次のメソッドを用いて設定する。


addHostKeyVerifier
コネクションの確立と鍵交換において、公開鍵の正当性を確かめるインスタンスを追加する。
addAlgorithmsVerifier
接続要求のアルゴリズムの正当性を確かめるインスタンスを追加する
loadKnownHosts
known_hostsファイルを読み、それに記述されているホストを正当な接続先とする
useCompression
データを圧縮して通信を行う (JZLib が必要)
setSocketFactory
ソケットのファクトリインスタンスを設定する
setTimeout
タイムアウト時間を設定する(ミリ秒)
setConnectTimeout
接続に関するタイムアウト時間を設定する(ミリ秒)

Facade を崩さない

このままではロジック部分が SSHJ を意識しなければならず、 SSHJ 特有の設定が SSHClientFacade 外部に漏れてしまう。
これでは、Facade を作った意味がない。なので、これらアクセス方法を定義した Strategy パターンを Enum 型で実装し、利用する。

実装一例。下記を SSHClientFacade に追加。

	/**
	 * Definition how to access to your server. 
	 *
	 */
	public static enum AccessStrategy {
		SERVER_A {
			@Override
			void execute(SSHClient ssh) throws IOException {
				ssh.setConnectTimeout(60 * 1000);
				ssh.setTimeout(30 * 60 * 1000);
				ssh.loadKnownHosts();
				ssh.addHostKeyVerifier(new PromiscuousVerifier());
			}			
		},
		SERVER_B {
			@Override
			void execute(SSHClient ssh) throws IOException {
				ssh.setConnectTimeout(10 * 1000);
				ssh.setTimeout(10 * 60 * 1000);
				ssh.loadKnownHosts();
			}
		};

		/**
		 * write your settings.
		 * @param ssh
		 * @throws IOException
		 */
		abstract void execute(SSHClient ssh) throws IOException;
	}
	/**
	 * Access server with your customize settings.
	 * @param host
	 * @param port
	 * @param method
	 * @throws IOException
	 */
	public void connect(String host, int port, AccessStrategy method) throws IOException {
		method.execute(ssh);
		ssh.connect(host, port);
	}

ロジック部の使用例

	ssh.connect(property.getString("ssh.host"), property.getInt("ssh.port"), AccessStrategy.SERVER_A);

SSH はセキュリティ上重要なため、ライブラリ開発者の都合によっては、今後取り替えることを想定すべきかもしれない。
もしそうであったとしても、おそらくサーバの接続方法や設定は概念的には変わらない。
このように設計しておけば、ロジックの変更は SSHClientFacade クラスを変更するだけなので、取り替えは容易になる。
(またこのメソッドを直接ユーザに使わせないよう、ストラテジーのメソッドアクセスレベルはパッケージにしている)
ただし、この拡張はコードが複雑になるため、本当に必要なときだけ行った方が良い。

また、ここでは触れないが、認証方法に関しても様々なものが用意されているため、
本家のリファレンスは見ておくといいかもしれない。

C言語 の long int データ型仕様

概要

今回、開発環境でテストすると正しいが、本番環境では正しい結果が出力できなかった。
あまりにバカらしい理由ではあるが、戒めのために記述する。

起こった事

環境

この案件では、C 言語で記述したプログラムを本番環境に送って実行する。
私は、Windows 7 64bit のデスクトップで cygwingcc (4.9.3) を使って開発。

問題箇所

ある計算結果を出力するのだが、その結果は結構大きな値になってしまうので、
結果を格納するデータの型に long int を使用。そして、結果の表示において最初以下のように記述していた。

	long int result = (計算部);
	printf("%d", result); // 588491482334(期待値)

すると、出力結果が明らかに小さい。

80962782

そして、自分が フォーマッタを『%d』にしていたミスに気付く。
『%d』→ 『%ld』に修正して、テスト。

	long int result = (計算部);
	printf("%ld", result); // 588491482334(期待値)

出力結果が期待されるものになった。

588491482334

これで問題ないと思い、本番環境に送信して実行。その出力結果。

80962782

なぜか出力が戻った。送信したプログラムを確認したが問題ない。

問題解決フェーズ

計算結果を小さくしてみると正しい値が表示されるため、この現象は計算結果が大きくなった時のみ起こる。
しかし、フォーマッタは『%ld』を指定しているので、問題ないはずだが・・。
試しに『%lu』に変更してみたが効果なし。

今は C 言語の仕様が変わったのか?と思い、C 言語の仕様書を漁るもどこも変更なさそう。

仕方がないので本番環境で試しに、『LONG_MAX』(limits.h)を出力してみた。

2147483647

この瞬間、計算期待値がlong int の表現範囲を超えてることに気づく。
そこで、下記のように修正。

	long long int result = (計算部);
	printf("%lld", result); // 588491482334(期待値)

本番環境でも動作を確認。こんな単純な問題に2時間掛けてしまった。

C 言語のデータ型仕様

この問題の要因を作った、long int の表現範囲を調べる。
まず、一般的には(データ型を参照)

int → 2byte(-32768~32767)を記憶できる
long int → 4byte (-2147483648~2147483647)を記憶できる
long long int → 8byte (-9223372036854775808~9223372036854775807)を記憶できる

まず、ここで既に勘違い。
long int が 8byte だと思ってた。(Java の long 型が 8byte だから)
(付け加えるなら long long int は 16byte だと思ってた)

そして、仕様は結構アバウトであり、基本的に下限だけ決まっている。
つまり、byte 数が大きい分には構わない。実際、私の開発環境では

#include<stdio.h>
#include<limits.h>

int main() {
	printf("INT_MAX:\t%d\n", INT_MAX);
	printf("LONG_MAX:\t%ld\n", LONG_MAX);
	printf("LLONG_MAX:\t%lld\n", LLONG_MAX);
	return 0;
}

その結果。

INT_MAX:        2147483647
LONG_MAX:       9223372036854775807
LLONG_MAX:      9223372036854775807

long int は 8byte だった。しかも、 long long int とデータサイズ同じだった。
普段そんなに大きな値を使わないから、全く意識できていなかった。
反省。

結論

基本的なことではあるけれど、データ仕様は正しく覚える。
環境によってデータサイズが異なる事にも留意する。

各ケースのデータ選択には分岐より規約に基づいた処理を適用する

概要

分岐処理は、手軽に記述できるため、多くの場面で利用される。

しかし、あるデータの値によって、途中のデータを選択する処理を分岐を用いて
実装してしまうと、全てのケースをカバーしなければならないため、
新たなケースの追加や既存のケースの修正によって、プログラムを修正する必要がある。

もし、これら各ケースをプログラムでは同じ処理だと認識できれば、
ケースの追加やケースの修正を感知しないため、もっと保守性を向上できる。
今回はこれについて説明する。

詳細説明

今回は、会員のカテゴリ(VIP, 会員, 無料会員)があり、
これらのカテゴリ毎にメッセージを別々に表示したいとしよう。
まずは、改善の余地がある処理を提示する。

package sample;

import resources.properties.Property;

public class Main {

	public static void main(String[] args) {
		
		Property property = new Property("message");
		String title, message;
		
		// ここで会員カテゴリを取得するとする.
		String category = getMemberCategory();
		// ここでデータを選択する.
		if(category.equals("A")) {
			title = property.getString("vip.title");
			message = property.getString("vip.message");
		}else if(category.equals("B")) {
			title = property.getString("member.title");
			message = property.getString("member.message");
		}else if(category.equals("C")) {
			title = property.getString("normal.title");
			message = property.getString("normal.message");
		}else {
			throw new IllegalStateException("カテゴリ名が不正です");
		}
		// データを用いた処理.
		System.out.println("title: " +  title);
		System.out.println("message: " +  message);
	}
	// コンパイラを通すために仮の値を作成.
	private static String getMemberCategory() {
		return "B";
	}
}

なお、property クラスは、message.properties からキーに対応した値を取得するクラスである。
以下の過去記事にその実装例がある。
プロパティファイルの使用方法 - Status Code 303 - See Other
コンストラクタでリソースファイル名を指定できる実装にしているが、基本的に動作は同じ。

そして、タイトルやメッセージを格納した message.properties はこちら。

vip.title = VIP会員様へ
vip.message = 今お買い物頂くと 40% のポイントが溜まります。
member.title = 会員様へ
member.message = 今お買い物頂くと 20% のポイントが溜まります!
normal.title = 無料会員様へ
normal.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます!

タイトルとメッセージを外出しファイルにすることで、内容の変更は properties ファイルの
更新だけになり、プログラムを修正する必要がないようにしている。

しかし、もうちょっと保守性を向上できる。
今回、ネックとなっているのは分岐の部分だ。

分岐を用いた、このような実装は自然に映るかもしれないが、
会員カテゴリが追加、変更された場合は、以下を変更しなければならない。

  • プログラム(if 文の分岐部分)
  • リソースファイルの xxx.title, xxx.message キーの追加

本来、プログラムは処理を記述するものであるから、データ追加の影響は受けたくない。
タイトルとメッセージだけを追加すれば動作するようにしたい。

これを実現するには、下記のように message.properties のキー名を
カテゴリに対応したキー名にし、それを処理で取得するようにする。
今回の例では、次のようにする。

  • [カテゴリ名] + ".title" の値をタイトルに表示する
  • [カテゴリ名] + ".message" の値を本文に表示する

このように、リソースデータの記述方法を規約化してしまうことによって、
各ケースに対する処理をまとめてしまう。

package sample;

import java.util.MissingResourceException;

import resources.properties.Property;

public class Main {

	public static void main(String[] args) {
		
		Property property = new Property("message");
		String title, message;
		
		// ここで会員カテゴリを取得するとする.
		String category = getMemberCategory();
		try {
			// ここでデータを選択する.
			title = property.getString(category + ".title");
			message = property.getString(category + ".message");

		} catch(MissingResourceException e) {
			throw new IllegalStateException("カテゴリ名が不正です");
		}
		// データを用いた処理.
		System.out.println("title: " +  title);
		System.out.println("message: " +  message);
	}
	// コンパイラを通すために仮の値を作成.
	private static String getMemberCategory() {
		return "B";
	}
}

message.properties は以下のようにキー名を変更する。

# 存在するカテゴリ名 + .title, + .message をデータとして取得する
A.title = VIP会員様へ
A.message = 今お買い物頂くと 40% のポイントが溜まります。
B.title = 会員様へ
B.message = 今お買い物頂くと 20% のポイントが溜まります!
C.title = 無料会員様へ
C.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます!

このようにすれば、もし、カテゴリが追加されたとしても、message.properties に
新しいカテゴリ "D" に対する title と message を追加すれば、処理は変更しなくて良くなる。

# 存在するカテゴリ名 + .title, + .message をデータとして取得する
A.title = VIP会員様へ
A.message = 今お買い物頂くと 40% のポイントが溜まります。
B.title = 会員様へ
B.message = 今お買い物頂くと 20% のポイントが溜まります!
C.title = 無料会員様へ
C.message = 正会員になれば、ポイントが溜まり、1p=1円でご利用できます!
# 追加
D.title = お試し会員様へ
D.message = お試し期間中は全サービスが使えますが、有効期間は1週間です!

その出力結果。(getMemberCategory() で "D" を返すように変更)

title: お試し会員様へ
message: お試し期間中は全サービスが使えますが、有効期間は1週間です!

今回は、簡単のため、同一 properties ファイル内にデータを記述しているが、
カテゴリによって変更されるデータの数が多くなればなるほど、大量のデータで混沌とする。

この場合は、読み込むリソースファイル自体を変更する。
例えば カテゴリが "A" の場合、message_A.properties を読み込むようにする。

注意点として、この方法は各ケースの異なる部分が処理データだけだということ。
もし、各ケースの処理自体が異なっている場合は、単純にこの方法は適用できない。

もし、データ選択とそのデータの処理がコード的に分割できるのであれば、
まずそれを行ってから、上記変形を行う。

結論

データの値によって、処理されるデータを変更したい場合は、
リソースファイルに規約を適用した処理を記述することによって、
ケースの追加や変更にリソースファイルだけで対応できるようになる。

しかし、各ケースによって処理自体が異なってしまう場合は、単純に適用できない。
もし、これらからデータ選択部と処理部が分割できるなら、それをまず行う。

テンプレートによるロジックとデザイン切り分け

概要

デザインとロジックを切り分ける内容。
本来これらは全く別個に管理すべきなものであるため、分離させたい場合が多くある。
今回は、Java でこの方法を実現する手順を記述。

デザインとロジックの混在する悪い例

public class Main {

	public static void main(String[] args){
		String user = "hoshi-kouki";
		boolean isMember = true;
		Cart cart = new Cart();
		cart.add(new Item("タマネギ", 100));
		cart.add(new Item("じゃがいも", 150));
		cart.add(new Item("人参", 100));
		cart.add(new Item("肉", 500));

		System.out.println(user + " 様");
		System.out.println();
		System.out.println("以下商品を購入されたことを通知します。");
		System.out.println("===========================");
		for(Item item : cart.getItems()) {
			System.out.println(item.getName() + "		"  + item.getPrice() + " 円");
		}
		System.out.println("--------------------------");
		System.out.println("合計		" + cart.sum() + " 円");
		System.out.println("--------------------------");		
		if (isMember) {
			System.out.println("いつもお買い上げ頂きありがとうございます!");		
		}else {
			System.out.println("会員登録するとポイントがもらえますよ!");		
		}
	}	
}

まずい理由

  • ロジックとデザインが混在するため、どちらも内容が分かり辛い。
  • デザイン変更→ロジック破壊、ロジック変更→デザイン破壊が起こる可能性
  • デザインを管理すべき人が自由に変更できない
    • デザイン担当する人がプログラマと同じことができるとは限らない

本来あるべき姿

  • デザインを担当する人は、その人の好きなツールで作成
  • そのデザインを内部に取り込む仕組みはプログラマが作成

テンプレートの仕組み作成

Velocity - Velocity User Guide を用いたサンプルを記述する。
インストール手順は、ここ参照(http://hack.aipo.com/archives/8360/)。

事前設定
  1. プロジェクト作成
  2. velocity.zip ダウンロード
  3. 展開 → velocity-x.x-dep.jar (x.xはバージョン) プロジェクト内にコピー
  4. ビルドパスに velocity-x.x-dep.jar 設定
詳細説明

今回実装したテンプレート利用クラス(OutputBuilderクラス).

import java.io.StringWriter;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

/**
 * 既に定義されたテンプレートファイルからプログラム上で様々な値を
 * セットして処理に応じた結果を作成する。
 */
public class OutputBuilder {

	/**
	 * ファイル名.
	 */
	private String fileName;
	
	/**
	 * Velocity コンテキスト.
	 */
	private VelocityContext context;

	/**
	 * コンストラクタ.
	 * @param fileName ファイル名
	 */
	public OutputBuilder(String fileName) {
		this.fileName = fileName;
		context = new VelocityContext();
	}

	/**
	 * Velocityコンテキストにパラメータを設定する.
	 * @param key キー名
	 * @param value
	 * @return 呼び出したテンプレートビルダー自身
	 */
	public OutputBuilder set(String key, Object value) {
		context.put(key, value);
		return this;
	}

	/**
	 * 格納した結果から文字列を作成する.
	 * @return 文字列
	 */
	public String build() {
		Velocity.init();
		Template template = Velocity.getTemplate(fileName, "UTF-8");
		StringWriter writer = new StringWriter();
		template.merge(context, writer);
		return writer.toString();
	}
}

今回は最小構成で作るため最低限のみ。機能追加は本家ドキュメントを参考に。

使い方

請求書を作成するとして記述する。

  1. 必要データ洗い出し
    請求書に必要なデータは、今回は以下とする。

    • 購入者
    • 購入商品・その値段
    • 合計金額
    • 購入者が会員/非会員かどうか


  2. Value Object(VO)用意
    基本データ(文字・数値)のみで構成しても動作するが、モデル化クラスを利用した方が分かりやすくなる。

    • 商品の属性情報を管理するクラス。(Itemクラス)
    • 商品自体を管理するクラス (Cartクラス)


  3. 各クラス実装
    Itemクラス

    /**
     * 商品情報を管理する.
     */
    public class Item {
    	/**
    	 * 商品名.
    	 */
    	private String name;
    	/**
    	 * 単価.
    	 */
    	private int price;
    	/**
    	 * コンストラクタ.
    	 * @param name 商品名
    	 * @param price 単価
    	 */
    	public Item(String name, int price) {
    		this.name = name;
    		this.price = price;
    	}
    	public String getName() {
    		return name;
    	}
    	public int getPrice() {
    		return price;
    	}	
    }
    

    Cartクラス

    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 購入商品を管理する.
     */
    public class Cart {
    	/**
    	 * 購入商品.
    	 */
    	private  List<Item> items;
    	/**
    	 * コンストラクタ.
    	 */
    	public Cart() {
    		items = new ArrayList<>();
    	}
    	/**
    	 * 商品リストを取得する.
    	 * @return 商品リスト
    	 */
    	public List<Item> getItems() {
    		return items;
    	}
    	/**
    	 * 商品を items に追加する.
    	 * @param item 追加する商品
    	 */
    	public void add(Item item) {
    		items.add(item);
    	}
    	/**
    	 * 商品の合計金額を取得する.
    	 * @return 金額
    	 */
    	public int sum() {
    		int sum = 0;
    		for(Item item : items) {
    			sum += item.getPrice();
    		}
    		return sum;
    	}
    }
    


  4. テンプレート(bill.vm)作成

    $name 様
    
    以下商品を購入されたことを通知します。
    ===========================
    #foreach ($item in $cart.getItems())
    $item.getName()		$item.getPrice() 円
    #end
    --------------------------
    合計		$cart.sum() 円
    ===========================
    #if($isMember)
    いつもお買い上げ頂きありがとうございます!
    #else
    会員登録するとポイントがもらえますよ!
    #end

    今回は紹介のため様々なものを使っているが、
    デザインの全体像が分かり辛くなるため分岐(#if ~ #else ~ #end)はあまり使わない方が良い。

    なお、上記にある特殊な意味を持つ文字について説明すると。

    $name

    setメソッドで渡したキー名'name'に対応した値を表示。

    $cart.getItems()

    setメソッドで渡したキー名'cart'に対応するデータに対して getItem メソッドを呼び出した結果を表示。

    #foreach ($item in $cart.getItems()) (処理) #end

    (処理)を集合個数($cartにはいったアイテム分)実行する。
    各繰り返し処理において、カートの中の各要素がループごとに $item にセットされ、処理に利用できる。

    #if($isMember) (処理1) #else (処理2) #end

    条件によって、(処理1)か(処理2)の結果が表示。


  5. ロジック記述
    ロジックにテンプレートに対する処理を記述。

    1. 使用するテンプレート指定 (OutputBuilder コンストラクタ)
    2. $name, $isMember, $cart に使用するデータ設定(setメソッド)
    3. テンプレートの指定箇所にロジックで得たデータを組み合わせた結果を得る(build メソッド)
    public class Main {
    
    	public static void main(String[] args) {
    		Cart cart = new Cart();
    		cart.add(new Item("タマネギ", 100));
    		cart.add(new Item("じゃがいも", 150));
    		cart.add(new Item("人参", 100));
    		cart.add(new Item("肉", 500));
    		
    		OutputBuilder template = new OutputBuilder ("bill.vm").set("name", "hoshi-kouki").set("isMember", true).set("cart", cart);
    		System.out.print(template.build());
    	}
    }
    

  6. 出力結果は以下。

    hoshi-kouki 様
    
    以下商品を購入されたことを通知します。
    ===========================
    タマネギ		100 円
    じゃがいも		150 円
    人参		100 円
    肉		500 円
    --------------------------
    合計		850 円
    ===========================
    いつもお買い上げ頂きありがとうございます!

    bill.vm の所定の箇所にデータが埋め込まれた形で出力できていることが分かる。

参考文献

Start up the Velocity Template Engine | JavaWorld
http://hack.aipo.com/archives/8360/

更新履歴

2016/01/20 変な表現多すぎたので、記事修正。
      TemplateBuilderクラス → OutpuBuilderクラス(テンプレート作るわけじゃないし)
2016/06/13 説明が冗長すぎて分からなかったので修正。