<!-- -*- mode:text coding:euc-jp -*-
   $FML: IO.sgml,v 1.7 2010/03/18 20:28:05 fukachan Exp $
-->

<chapter id="module.io.adapter">
	<title>
	IO へのアダプター層 (IO::Adapter クラス)
	</title>


<sect1 id="module.io.adapter.overview">
	<title>
	IO::Adapter の概要
	</title>

<para>
&fml8; のメンバーリストはファイル、
Unix のグループ、
RDBMS (リレーショナルデータベースシステム)、
LDAP など、さまざまな形式で管理できます。
もちろん、デフォルトはファイルです。
</para>

<para>
&fml8; でのメンバーリストの読み書き、
たとえばメールアドレスの登録/削除は
IO::Adapter という抽象クラスを通じて行ないます。
ようは Unix における vfs/vnode インターフェイスの導入です。
使い方は次のようになります。
<screen>
use IO::Adapter;
$io = new IO::Adapter $map, $map_params;
$io->open || croak("cannot open $map");
while ($x = $io->get_next_key()) { ... }
$io->close;
</screen>
</para>

<para>
$map は「map:識別子」形式です。
現在のところ $map は次のようなものが指定可能です。
なお file: は省略可能です。
<screen>
file:/var/spool/ml/elena/recipients
unix.group:root
nis.group:root
mysql:id
postgresql:id
ldap:id
</screen>
</para>

<para>
file: は通常のファイルです。
unix.group:root は
/etc/group の root のエントリにあるユーザのリスト、
nis.group:root は NIS の root のエントリにあるユーザのリストです。
mysql:id は MySQL、
postgresql:id は PostgreSQL へのアクセスです。
ldap:id は LDAP へのアクセスです。
</para>

<para>
設定ファイル中では、(なにかとまぎらわしいので)、
RDBMS と LDAP を別の名前空間で定義する仕様です。
たとえば MySQL は config.cf の mysql:id で指定されたパラメータを用います。
<screen>
[mysql:id]

sql_database                                      = fml

sql_password                                      = uja

... 略 ...
</screen>
このパラメータは new IO::Adapter を実行する際に、
指定する必要があります(後述)。
</para>

</sect1>


<sect1 id="module.io.adapter.methods">
	<title>
	IO::Adapter のメソッド
	</title>

<para>
現在、IO::Adapter の提供する公式なメソッドは次の通りです。
<screen>
new()
open()
close()

get_next_key()

add(KEY)
delete(KEY)

getpos()
setpos(NUM)
eof()

touch()

find(REGEXP, $args)
</screen>
</para>

<para>
KEY はデータベースのプライマリキーで、
通常メールアドレスです。
REGEXP は正規表現ですが、たいていはメールアドレスです。
</para>

<para>
多くのマップを同じようにあつかうために、
データの操作方法を考え、
最少限の少数精鋭のメソッドだけを実装しています。
</para>

<para>
メソッド群の定義は、実際に実装し、実験運用を通じて選ばれました。
その際、
ファイルの IO よりは、SQL との IO を想定しつつ、
メソッドの仕様を考えています。
そのため、&fml4; のファイルを前提にした処理とくらべると、
少し不自由に思えるでしょうが、
そのぶん抽象度合いは高くなっています。
そして、
これを実装し、長年運用できているので、
現在の仕様で必要十分のようです。
</para>

</sect1>


<sect1>
	<title>
	&fml8; で必要なメソッドの型
	</title>

<para>
メソッドの型は、
get_next_key()
のように引数なしか STR を引数にし STR を返り値にしています。
これは、
ファイルの一覧を見るとか、
特定のメールアドレスに対して何かの値を取り寄せる、
といった操作が多く使われるためです。
</para>

<para>
しかし、特定のアドレスに対する値が組になっている返り値もあります。
これは
<screen>
KEY_STR => [
     VALUE_STR_1
     VALUE_STR_2
     VALUE_STR_3
]
</screen>
のようなデータ構造が返ってくることを期待する場合です。
たとえば
&fml4; の actives は、
メールアドレスに対していくつかのオプションの値を記述するので、
こういったデータ構造と考えられます。
<screen> 
例

rudo@nuinui.net	s=skip m=xxx.yyy.z # commnet

rudo@nuinui.net => [
        s=skip
        m=xxx.yyy.z
        # comment
]
</screen> 
</para>

<para>
まとめると、
引数は「ない」か「STR」(正規表現も文字列の一種)なので 
STR 型と一まとめに考えてかまいません。
一方、返り値は STR か ARRAY_REF になります。
<screen>
引数    返り値
---------------------------------------
なし => STR

STR  => STR

なし => [STR, STR, ... ]

STR  => [STR, STR, ... ]
</screen>
</para>

</sect1>


<sect1 id="module.io.adapter.map.file">
	<title>
	file マップ	
	</title>

<para>
「file:ファイル名」
もしくは単なる
「ファイル名」は、
ごく普通のテキストファイルへの IO を抽象化したものです。
</para>

</sect1>


<sect1 id="module.io.adapter.map.unixgroup">
	<title>
	unixgroup マップ	
	</title>

<para>
/etc/group を抽象化します。
当然 READ ONLY です。
</para>

<para>
/etc/group の
<screen>
wheel:*:0:root,rudo,kenken
</screen>
は IO::Adapter では unixgroup:wheel としてアクセスできます。
<screen>
$io = new IO::Adapter "unixgroup:wheel";
</screen>
というオブジェクトに対し get_next_key() すると
グループのメンバーが順に返されます。
ちょうど
<screen>
root
rudo
kenken
</screen>
のようなファイルとみなしています。
つまり
/etc/group (上述の行)を、
合計3行のファイルを読みだす処理と同じと見なしています。
</para>

</sect1>


<sect1 id="module.io.adapter.map.nis">
	<title>
	NIS マップ	
	</title>

<para>
/etc/group のケースとほぼ同様ですが、
ソースが
NIS/YP から group にあたる情報を出してきている点だけが異なります。
</para>

</sect1>


<sect1 id="module.io.adapter.map.mysql">
	<title>
	MySQL マップ	
	</title>

<para>
運用部隊としては
「保守性をあげるためには、一箇所に設定をまとめておきたい」
つまり
「一つの cf ファイルにいろいろ書けるようにしておきたい」
と考えます(あとで diff を取るとか考えると、このほうが便利です)。
そこで、
&fml8; の場合、
config.cf の中に SQL の設定も書けるなら書いておきたいと考えます。
</para>

<para>
そこで、
[mysql:members]
(このキーワードから終りまで、もしくは次の =head 文まで)のブロックで、
マップのタイプと識別子を表現しています。
<screen>
config.cf の記述例

member_maps     =       mysql:members

recipient_maps  =       mysql:recipients

[mysql:members]

sql_server      =       localhost
sql_user        =       fml
sql_password    =       uja
sql_database    =       fml
sql_table       =       ml

sql_find	=	select * from ...

		...
</screen>
config.cf の中に別の名前空間を定義していると考えてもらってかまいません。
ここは Unix より MS-DOS の config.ini 形式風ですが、
Unix では該当するものが見つからないので .ini 風となりました。
</para>

<para>
RDBMS や LDAP に対し $config の情報が必要なため、
IO::Adapter は
<screen>
new IO::Adapter "mysql:members", $config;
</screen>
のように呼び出す必要があります。
よって、各ソフトウエアでは、つねに 
<screen>
new IO::Adapter $map, $config; 
</screen>
と書いておく必要があります。
この $config は、
次のようなハッシュ・リファレンスになっていることが期待されています。
通常 $config は FML::Config オブジェクトそのものです。
<screen>
$config => {
	[mysql:members] => {
		sql_sever => localhost
			...	
	}
}
</screen>
FML::Config は
.cf から上のようなハッシュ・リファレンスを構成します。
よって IO::Adapter で new() を呼び出す際には、
つねに FML::Config オブジェクトを引数で渡すようにしてください。
</para>


<sect2>
	<title>
	議論:
		SQL 構文を config.cf に書くには？
		(参照: fml-devel 204)
	</title>

<para>
関数が呼ばれた際に、
初めて決まる変数もあるので config.cf で定義は完結しません。
lexical scope で取り扱われる変数の表現が必要になります。
現在は、この目的のために &amp;varname 記法(下記)を使っています。
</para>

<para>
ただ、どこで呼ばれるか？を考えないと分からないので、
利用可能なローカルスコープの変数一覧表を作るのは困難です。
どうしたものかというところですが…
</para>

<para>
例: 
以下は、
受信者リストとメンバーリストが食い違う例です。
メールアドレスの登録をする際に、fml_address という値が異なります。
この場合、
member と recipient は少し異なる SQL 命令になるので別々に定義しています。
<screen>
member_maps 	= 	mysql:members

recipient_maps 	= 	mysql:recipients


[mysql:members]

sql_server	=	localhost
sql_user	=	fml
sql_password	=	uja
sql_database	=	fml
sql_table	=	ml

sql_get_next_key =	select fml_address from $sql_table
			where fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'

sql_getline	=	select * from $sql_table
			where fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'

sql_add		=	insert into $sql_table
			values ('$ml_name', '$ml_domain', '&amp;address', 1, 1)

sql_delete	=	delete from $sql_table
			where	fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'
					and
				fml_address = '&amp;address'

sql_find	=	select * from $sql_table
			where	fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'
					and
				fml_address like '&amp;regexp'



[mysql:recipients]

sql_server	=	localhost
sql_user	=	fml
sql_password	=	uja
sql_database	=	fml
sql_table	=	ml

sql_get_next_key =	select fml_address from $sql_table
			where fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'
					and
				fml_recipient = '1'

sql_getline	=	select * from $sql_table
			where fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'
					and
				fml_recipient = '1'

sql_add		=	update ml
			set recipient = 1
			where fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'
					and
				fml_address = '&amp;address'

sql_delete	=	update ml
			set recipient = 0
			where fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'
					and
				fml_address = '&amp;address'


sql_find	=	select * from $sql_table
			where	fml_ml = '$ml_name'
					and
				fml_domain = '$ml_domain'
					and
				fml_recipient = '1'
					and
				fml_address like '&amp;regexp'
</screen>
</para>

<para>
off/on 命令を無視するとか、
fml が SQL からデータを読むだけで書き込むことはしないのであれば、
話は簡単になるわけですが…
避けるのは難しいですね。
</para>

</sect2>


</sect1>


</chapter>
