<!--
   $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>
のようなファイルとみなされます。
つまり 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 ファイルにいろいろ書けるようにしておきたい。
たとえば &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>
