Ubuntu Server 24.04 LTSでINTER-Mediatorを稼働する

Ubuntu 24が正式にリリースされました。この上で、INTER-Mediatorを動かす方法をまとめておきます。期待通り、以前よりも追加モジュールは少なくて済むようになりました。セットアップ作業後、管理者アカウントで、以下のようにコマンドを入力します。

sudo apt -y -U upgrade
sudo apt install -y apache2 php mysql-server
sudo apt install -y nodejs
sudo apt install -y composer
sudo apt install -y php-xml php-gd
sudo apt install -y mysql-client php-pdo-mysql
sudo apt install -y postgresql php-pgsql
sudo apt install -y sqlite3 php-sqlite3

cd /var/www/html
sudo chown -R www-data:adm /var/www
sudo chmod -R g+rw /var/www
sudo systemctl restart apache2

git clone https://github.com/INTER-Mediator/INTER-Mediator.git
cd INTER-Mediator/
composer update

最初の塊は、必要なソフトウエアのインストールです。Linux自体のアップデートは、1つのコマンドでできるようになったので、早速使います。MySQLだけでなく、PostgreSQLやSQLiteについても記述しました。PHPのモジュールで足りないのは、xmlとgdだけでした。これだけのインストールで、最後のcomposer updateで必要なライブラリのダウンロードが成功します。これらでインストールされたソフトウエアのバージョンをまとめておきます。なお、Nodeについては、INTER-Mediatorのcomposer.jsonでも記述があり、そちらは、Ver.20をダウンロードして利用します。

  • Apache2:2.4.58
  • PHP:8.3.6
  • MySQL:8.0.36
  • Node:18.19.1
  • PostgreSQL:16.2
  • SQLite:3.45.1

2つ目の塊では、Apache2が稼働しているユーザwww-dataが、自分自身のホーム以下に書き込み権限があるようにしています。念の為、Apache2を再起動しています。この設定は、INTER-Mediatorの一部の機能では必要になります。

3つ目は、INTER-Mediatorのインストールです。なお、サンプルのデータベースを入れる必要があるので、続いて以下のようにコマンドを入れれば、3つのデータベースにサンプルデータベースのtest_dbがセットアップされます。最初にカレントディレクトリがINTER-Mediatorであることを前提にしています。特定のデータベースだけでいいのなら、他のデータベースについてのセットアップは無視してOKです。

cd dist-docs/
# MySQLのサンプルデータベース登録
sudo mysql -uroot < sample_schema_mysql.sql 

# PostgreSQLのサンプルデータベース登録
sudo -u postgres psql -c 'create database test_db;'
sudo -u postgres psql -f sample_schema_pgsql.sql test_db

# SQLiteのサンプルデータベース登録
sudo mkdir -p /var/db/im
sudo sqlite3 /var/db/im/sample.sq3 < sample_schema_sqlite.sql
sudo chown -R www-data:adm  /var/db/im

これで、「http://(サーバのIPアドレス)/INTER-Mediator/samples/」に接続すれば、INTER-Mediatorのサンプルの稼働が確認できます。

simplesamlphpのIdPでGoogle OAuth2認証を実行する

PHPでのSAML認証ライブラリであるsimplesamlphpは、PHPでSAML認証を行う場合にはほぼ一択となる選択肢です。これを利用してIdPを構築する方法は、https://blog.msyk.net/?p=1566 で紹介しました。本項目は、別掲の記事で紹介したIdPが存在する状態で、Google OAuth2、すなわちGoogleのアカウントでSAML認証を行えるようになるまでを紹介します。

とりあえずセットアップ方法とテスト方法は残しておかないといけないと思いつつ、まずは記事を書いたのです。INTER-MediatorでのGoogleアカウントでの認証は成功してはいませんでした。そして1日経過し、ふと疑問に思った事があります。simplesamlphpの追加モジュールとしてcirrusidentity/simplesamlphp-module-authoauth2を使うことにしています。このモジュールは、OAuth2の汎用モジュールなので、Google向けということではありません。さて、このモジュールは、IdPに必要なのか、SPに必要なのか、どちらなのでしょうか? このことは、ライブラリのインストールのページにも書いていません。当たり前のことなのかな?

以下の方法で、IdPにまずはインストールして設定を追加して、リダイレクトURLはIdP側にしたのですが、認証されている状態になりませんでした。そこで、全く同じようにSP側にもモジュールを追加し、リダイレクトURLをSP側にしましたが、なんと、全く同じ結果になりました。どちらでもいいのかもしれません。どちらでやっても同じ結果になったので、どっちが正しいのかという結論はとりあえずは出ていません。しかし、OAuth2の認証サーバがすでにあるという前提を考えれば、SPだけを用意してそれを利用する方が明らかに作業は少なくて済みます。ということは、SP側にモジュールを追加して運用するのが正しいような気がします。IdPに登録したユーザで認証する結果をみていると、一旦IdPに行ってログインパネルを出して、そこから、SP内部へリダイレクトして認証結果が伝わってくるように思えるので、やはりそのこともあって、SPへモジュールを追加するのが正しいように思います。

Googleのプロジェクトから認証情報を得る

SimpleSAMLphpの設定の前に、GoogleからクライアントIDとシークレットを取得します。その方法はあちらこちらのサイトで記載されていますので、例えば、Shinonome Tech BlogのOAuth 2.0 を使用してGoogle API にアクセスする方法 に記載された「1.Google API ConsoleでOAuth2.0の認証情報を取得」の作業を行なってクライアントIDとシークレットを取得します。以下、それぞれ、MYCLIENTID、MYCLIENTSECRETと記述します。つまり、これらのキーワードが登場したら、Google Developer Consoleで取得した実際の値に置き換えて設定等を行なってください。なお、「承認済みのリダイレクトURI」には、以下のURLを指定します。module.php以下のパスは常に同じですが、それ以前は実際にSPのサイトをどのようにセットアップしたかに依存します。

https://demo.inter-mediator.com/simplesaml/module.php/authoauth2/linkback.php

なお、実際のSPは「アプリケーション内のsimplesamlphp」でもあるので、エイリアスを使わないパスだとこのようになります。いずれにしても、Google側への登録と、後で説明する認証画面を出すためのURLの両方に、同一のリダイレクトURLを指定する必要があります。

https://demo.inter-mediator.com/saml-trial/lib/src/INTER-Mediator/vendor/simplesamlphp/simplesamlphp/public/module.php/authoauth2/linkback.php

SPにモジュールを追加する

SPで使用するモジュールは、https://github.com/cirrusidentity/simplesamlphp-module-authoauth2.git です。simplesamlphp自体にはGoogle OAuth2に対応した処理を実行できる素材はないので、外部から追加します。composer.jsonに組み込むか、あるいは次ようなコマンドで追加でインストールを行います。

composer require cirrusidentity/simplesamlphp-module-authoauth2

続いて、SP側のsimplesamlphpのconfig/config.phpファイルを修正します。まず、この先で、SAML認証結果を利用するアプリケーションのドメインを追加します。既定値では[]になっていますが、OAuth2のモジュールはこの設定を見ているようです。

'trusted.url.domains' => ['demo.inter-mediator.com'],

そして、OAuth2モジュールを利用可能にします。IdPとしてセットアップするときに値を変更した配列に、1行加えます。

'module.enable' => [
    'exampleauth' => true,
    'core' => true,
    'admin' => true,
    'saml' => true,
    'authoauth2' => true,
],

次に、config/authsources.phpに次の指定を加えます。’default-sp’などと同じ階層にキーとして’google’を指定し、値として配列を指定します。MYCLIENTID、MYCLIENTSECRETは実際に取得した値にもちろん置き換えて指定します。

'google' => [
   'authoauth2:OAuth2',
   'template' => 'GoogleOIDC',
   'clientId' => 'MYCLIENTID',
   'clientSecret' => 'MYCLIENTSECRET'
],

SP側の設定はとりあえずこれでOKのようです。SPの管理画面に、authoauth2モジュールが稼働していることが見えています。

なお、Testのところに「google」というリンクが出ており、クリックするとGoogleのページを表示しますが、認証処理までには至りません。リダイレクトのURLが設定されていないというようなメッセージが見えます。

認証プロセスを通して見る

ここで実際に使用しているアプリケーションのURLは「https://demo.inter-mediator.com/saml-trial/chat.html」です。このURLと最初の方でGoogle Consoleで指定したリダイレクトURL「https://demo.inter-mediator.com/simplesaml/module.php/authoauth2/linkback.php」がこの後必要になります。これらのURLに加えて、MYCLIENTIDを含め、以下のURLの文字列をエディタ上などで整えます。URLEncodingをしてもいいのですが、問題ある文字列はないので、そのままこの文字列に置き換えて利用してもいいでしょう。そして、このURLをブラウザのアドレス欄に貼り付けて、Enterキーを押し、GoogleのOAuth2の認証プロセスに入ります。なお、URLの細かい点では別の機会に改めてきちんと処理すると言うことにして、まずは動く状態にしたいと思います。

https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=MYCLIENTID&scope=openid%20email&redirect_uri=リダイレクトURL&state=authoauth2|security_token%3D333344445555%26url%3DアプリケーションURL&nonce=0394852-3190485-2490358&hd=gmail.com

stateはちょっと適当にしていますが、「authoauth2|」で始まっていないと行けないのは、このモジュールでの実装です。また、stateの中に「url=アプリケーションURL」と言う記述があって、そこが後から効いてきます。

ちなみに、OAuth2に対応したサイト等では、認証がなされていない場合に、GoogleやFacebookの認証プロセスに入るボタンを提供するかと思います。その場合、「Google」ボタンをクリックすると、上記のURLにリンクするような作りになっているはずです。FacebookならFacebookの認証システムへのURLにパラメータを設定してジャンプするようになっています。

URLをブラウザのアドレス欄に入れると、Googleの認証の画面になります。状況は人によって違うのでしょうけど、いずれにしても、Googleの認証の画面になって、ログインしていない場合はユーザ名とパスワードの入力を行ないます。

Googleの認証ページから、アプリケーションURLに指定したURLにリダイレクトされます。

実のところ「よしOK!」とは行きません。INTER-MediatorのSAML対応では、このままでは認証を継続させる事ができないので、これからチェックしないと行けないのですが、アプリケーションURLに対しては、それよりも先にリダイレクトURLにアクセスし、さらにそこからアプリケーションURLへリダイレクトされているようで、GETメソッドで何のパラメータもありません。SAMLのクッキーがHTTP Onlyで乗ってくるだけのようです。ちなみに、SAML認証が成功すると、SimpleSAMLとSimpleSAMLAuthTokenという2つのクッキーが入るのですが、現状、IdP/SPいずれの側を利用しても、前者のクッキーしか入らず、後者は入ってこないのです。

(続報:2024-3-9)その後、色々やってみたのですが、認証が成功した状態のクッキーになりません。と言うことで、コードを読みました。simplesamlphp-module-authoauth2には認証後に返ってくるURL(linkback.phpで終わるもの)が記載されており、実際にそのリダイレクトURIが呼び出されています。そこをチェックすると、前述の「https://accounts.google.com/o/oauth2/v2/auth」に対するstateパラメータの値は、次のようになるはずです。色々試しているうちに、セッションデータが落ちてきたのでわかったことです。

state=authoauth2|_796bed184adf107c10c0faf60c658c7da54d2b0972:アプリケーションURL

最初は「authoauth2|」である必要があります。リダイレクトされた最初の段階でチェックしています。続いて、|より後の文字列について、:で区切って、最初をid、最初の:以降をマージしてurlとしています。エラーがあると、そのurlにリダイレクトがします。セッションのデータが何かのきっかけで落ちてきた時に、そこからデータが取り出せるとしたら、|と最初の:の間は上記のコードか、もしくは「_eff7a14ba6adc55b6cd2f08b0399b747ed3ac9844e」である必要があります。このコード、ランダムに充てられている可能性もありますが、このデータで数回セッションデータが得られた後、セッションデータはほぼ空っぽのものばかりになってしまっていました。それで、SPの管理ページのログインテストを利用して、一度認証した状態にしておいて、アプリケーションを動かすと、今度は、セッションデータは何か大量に乗っかってきています。SimpleSAMLAuthTokenクッキーも来ました。しかし、その時のstateの|より後にくるべきコードは違うものでした。やはりランダムでした。このstateは最初にaccounts.google.comを呼び出すときに引数に指定しないといけないと言うことは、開発者が指定しないといけないと考えられますが、それを得る方法はわかりません。あるいは指定したコードが乗ってくるということなのかな?もうちょっとコードを追ってみようと思います。と言うことで、ここまでは行けました。ちょっとだけ進歩したかな。そうこうしているうちに、SimpleSAMLphpのVer.2.2.0が出ましたね。セッションのなかに、「アップデートしろー!」って情報がどっさり。

SimpleSAMLphp Ver.2を使ってみる(3)

(1)はIdPの起動、(2)はIdPの管理画面のチェックと進みました。ということで続いてSPです。ここでのSPはINTER-Mediatorで稼働しているという前提で話をします。状況としては次のようなものです。

  • 証明書を発行済みのドメインdemo.inter-mediator.com内で稼働する。DocumentRootは/var/www/demo_im_com
  • DocumentRootにsaml-trialディレクトリを作り、そこに、ページファイルchat.html、定義ファイルchat.phpを定義した
  • INTER-Mediatorは、saml-trial/lib/srcにgit cloneでインストールして、composerで必要なライブラリをインストール
  • 結果的に、SimpleSAMLphpのレポジトリのルートは、DocumenRoot以下、saml-trial/lib/src/INTER-Mediator/vendor/simplesamlphp/simplesamlphp となる
  • 設定ファイルのparams.phpは、saml-trial/lib/src/params.phpとする
  • demo.inter-mediator.comをホストしているApache2のsiteファイルでは、以下のように、/simplesamlへのエイリアスを作成する(Aliasの行は1行で記述)
<VirtualHost *:443>
    ServerAdmin info@inter-mediator.org
    DocumentRoot /var/www/demo_im_com
    ServerName demo.inter-mediator.com
Alias /simplesaml "/var/www/demo_im_com/saml-trial/lib/src/INTER-Mediator/vendor/simplesamlphp/simplesamlphp/public"

いきなり動くかなと確かめてみたら、ダメでした。composerの扱いをちゃんとやらないといけません。ここでは、composer.jsonのsimplesamlphp/simplesamlphpの値を”2.0.4″とバージョンをしっかり入れるようにしてみました。INTER-Mediatorの場合、composer clearnでライブラリを消して、composer update, composer installの順でコマンドを入れれば良いでしょう。

simplesamlphpの管理ページは、前回にも紹介したように、赤いヘッダなどがついたもので、CSSやスタイルシート、画像などが提供されています。ブラウザでパスを見る限りは、/simplesaml/assets/base…となっているので、レポジトリのpulic/assetsを見るのですが、空です。どうやら、assets以下の内容は、simplesamlphp/simplesamlphp-assets-baseという別のパッケージにあるようで、これが読み込まれていません。この別パッケージをassets以下に展開するには、composer installが必要なようで、結果的にupdateとinstallは両方行う必要があるようです。

設定ファイルの記述

これまでのセットアップを行うと、SimpleSAMLphpのSP自体は、パスがちょっと長いですが、/var/www/demo_im_com/saml-trial/lib/src/INTER-Mediator/vendor/simplesamlphp/simplesamlphpに存在することになります。以下のこのパスを「SPのルート」と記載します。このディレクトリの、configに設定ファイル、metadataにメタデータファイル、certに証明書類を入れるのが基本です。以下、参考にコマンドを記述しますが、INTER-Mediatorではもう少し手軽にする方法を用意していて、近々、これをSimpleSAMLphp Ver.2向けに更新する予定です。

まず、通信暗号化のための証明書を作ります。この証明書はサイトのTLSのための証明書を使ってもよく、実際には案件ではそのようにしましたが、SimpleSAMLphpのサイトの説明では、10年期限の自己署名証明書を作っています。サイトの証明書はつまり「自己署名だとダメかも」と思って使っていたわけですが、本家の説明がいきなり自己署名なので、単に暗号化のためだけに使っているということですね。opensslコマンドの後に属性などを入力しますが、(1)のIdPのところと同様適当に入れればいいかと思います。-outと-keyoutの後のファイル名も適当に指定します。

cd cert
openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out sp.crt -keyout sp.pem

SPのルート以下、configディレクトリには、元からあるconfig.php.distからコピーしたconfig.phpを用意します。そして、その内容を変更します。ポイントは以下の点です。baseurlpathは、SPのルートのpublicを参照するようにします。以前はwwwを参照していましたが、Ver.2で変わっています。残り3つの設定は、IdPと同様ですので、(1)の記事を参照してください。

'baseurlpath' => 'saml-trial/lib/src/INTER-Mediator/vendor/simplesamlphp/simplesamlphp/public/',
'technicalcontact_email' => 'your_email',
'secretsalt' => 'your_salt',
'auth.adminpassword' => 'your_admin_pass',

SPのルート以下、configディレクトリには、元からあるauthsources.php.distからコピーしたauthsources.phpを用意します。以下のように、default-spキーの配列の要素に、certificateとprivatekeyのエントリーを用意して、ここで作成したキーファイルと証明書ファイルを指定します。そして、entityIDをサイトのドメインに設定しておきます。

'default-sp' => [
  'saml:SP',
  'certificate' => 'sp.crt',
  'privatekey' => 'sp.pem',

   // The entity ID of this SP.
   'entityID' => 'https://demo.inter-mediator.com/',
   :

SPのルート以下、metadataディレクトリには、元からあるsaml20-idp-remote.php.distからコピーしたsaml20-idp-remote.phpを用意します。このファイルの最後(とはいえ、中身は短いコメントがあるのみ)に、IdPの管理ページからコピーした配列をコピーしておきます。

SPの管理ページからメタデータを取得

ということで、インストールに少しハマってしまいましたが、なんとか動きました。一応のルートは、https://demo.inter-mediator.com/simplesaml ですが、こちらは「ようこそ」と出るだけです。SPの管理ページに行くには、このURLの後にadminをつけた、https://demo.inter-mediator.com/simplesaml/admin にアクセスします。そして、config.phpで指定したパスワードを入力して、管理者として認証します。

設定のページは諸々確認できますが、ModulesのところでIdPとしては稼働していないことなどが分かります。

Testのタブでdefault-spのリンクをクリックすると、次のような画面が見えており、登録したIdPを認識していることが分かります。ただ、ここで「選択」をクリックするとエラーになるので、まだ何か問題なのかもしれません。

連携のところで、「V」の部分をクリックすると、メタデータが表示されます。このメタデータを、IdPに登録します。IdPがSimpleSAMLphpなら、metadata/saml20-sp-remote.phpファイルに追記することになります。

認証できています

それでは実際にIdPで認証したユーザで、INTER-Mediatorのアプリケーションを使ってみます。通常、ログインパネルが出てくるとこが、IdPというか、SPの画面に行きます。ここでは、まず、IdPを選択します。

すると、ログインパネルが出てきます。こちらはドメインを見ればわかるように、IdP側に切り替わっています。ここでは、テストユーザのuser01でログんを試みます。

無事にログインができ、メッセージが見えています。

ちなみに、SAML-tracerを使って追っかけてみました。チャットのアプリケーションのURL(https://demo.inter-mediator.com/saml-trial/chat.html)をブラウザに入れると、何度かリダイレクトされて、IdPの側の認証ダイアログが表示されます。そこまでのトレースは以下の通りです。

続いて、正しいユーザとパスワードを入力して、IdPにポストしますが、その後、アプリケーションのURLにリダイレクトされています。この時は、認証が通っているので、アプリケーション側でも、認証が通った後の処理をして、ページが構築されています。

ということで、SimpleSAMLphp Ver.2.0.4でも動くことを確認しましたが、途中ちょっとハマった理由は、すでにVer.3の作業に入っていることに気づかず、dev-masterで作業したら、色々思った通りに動かなかったのでした。Packagistのサイトを見て、あ、Ver.3.0.0になっていると気づき、Ver.2.0.4で通るようにやり直して稼働を確認できたという次第です。ちゃんとチェックしようねってことですね。

SimpleSAMLphp Ver.2を使ってみる(2)

前の記事では、テスト用のIdPを起動するところまでを説明しました。Ver.2ではIdPの管理画面も新しくなっているので、続いてその管理画面に何が出ているかを確認しましょう。

まず、画面上部のタブ「設定」のページです。最初にSimpleSAMLphpのインストール場所やバージョンが見えています。正しく、Ver.2.0.4がインストールされていると判断できるでしょう。そして、インストールされているモジュールや動作チェックなどがあります。Ver.2になった変更点として、プラグイン的に必要な機能は追加するようになったと記載があり、必要な素材が全部入っている状態ではありません。必要な機能があるのなどはこの画面などでのチェックも必要かもしれません。

前の画面のDetailsにある「Information on your PHP installation」のリンクは、phpinfo()関数を動かした結果を表示します。「ホストネームやポート、プロトコルを診断」は次のような画面を表示します。サーバがきちんと動くようなら、特に確認は不要かもしれません。

「Test」のタブでは、admin、default-sp、example-userpassの3つのリンクがあります。まず、adminは次のように、管理者ログインに関する情報が見えています。

「default-sp」をクリックしても「No identity providers found. Cannot continue.」と出るだけです。これは正しい状態なのか、追々調べます。

example-userpassをクリックすると、次のようにログインパネルが出て、ログインの検証が可能です。ここで、config/authsource.phpで定義したユーザとパスワードを入れてログインをしてみます。

正しいユーザ名とパスワードを入れれば認証が行われて、その時に得られる属性についても表示されます。

ページ上部の「連携」をクリックすると、次のような表示が見えます。SPが2つになってしまっていますが、idpのドメイン名を設定した側を利用するものとして想定します。ここでは、中央付近に見えているボックスの下部にある「V」部分をクリックします。

V部分をクリックすると、表示が開いて、IdPのメタデータが表示されます。上部が一般的なXMLによる記述で、下部がSimpleSAMLphpで利用できるPHPの配列形式のメタデータです。ともかく、SPとの連携の時のデータは取り出しができるようです。

以上のように、IdPの管理画面としては、以前より少しは機能が増えたものの、SPの登録などはないようなので、やはり基本は設定ファイルを修正するということになるでしょう?認証可能かどうかやインストール状態などの動作チェック等にはある程度は利用できそうです。

SimpleSAMLphp Ver.2を使ってみる(1)

PHPでSAML認証を実現するライブラリ、SimpleSAMLphpが、2023年からVer.2となりました。SAML 2.0に対応するのは以前から、つまり、SimpleSAMLphp Ver.1でもSAML 2.0に対応していましたが、どちらのバージョンも「2」になったということです。バージョン記述がややこしいですが、まあ、これを読んでいる方は慣れているかと思いますので、先に進みます。

この記事は2023 7/1に最初に記述しましたが、状況が変わりつつあるのとノウハウが少し溜まったこともあって、2024/3/2までに追記を何度か行なっています。

INTER-MediatorはSimpleSAMLphpベースでSAML対応しています(勉強会での発表ビデオはこちらです)。SAMLというか、Shibboleth認証の案件を実際に行ったこともあります。ということで、SimpleSAMLphp Ver.2は早めにチェックしようと思いつつ、今になってしまいました。

SimpleSAMLphp Ver.2になっての違いはこちらのページに記載されています。かいつまんで説明すると、Shibboleth 1.3、SAML 1.1にはもう対応しないということで、SAML 2.0のみ対応となっています。ということは、Shibboleth案件は、Ver.1.19.xあたりで作業する必要があるということになります。設定ファイル名は変わっていないものの、「作り直したほうがいい」となっていますので、手順を含めて、引き続いてそのあたりは説明したいと思います。それから、いくつかの重要なパスも変わっています。これも説明で紹介します。

INTER-MediatorのSAMLのテストは、SimpleSAMLphpによるIdPと、SimpleSAMLphpによるSPを使って行うようにセットアップをしてあるのですが、改めて、この環境を構築し直しを始めました。その記録をブログにつけていこうと思います。IdPには、テスト用のアカウントをいくつか記録する程度で、そこから別の認証サービスを使うまではとりあえずは考えていません。

テスト環境ですが、Ubuntu Server 22.0.4 LTSです。よって、PHPは8.1です。普通に、Apache2、PHPとモジュールをインストールしました。INTER-Mediatorをインストールする以外には、PHPのSOAPモジュールを追加するだけで大丈夫でした。 テスト用のアプリケーションも当然ながらINTER-Mediatorで作ってあるのですが、SimpleSAMLphpのVer.1とVer.2の相互運用も考えないといけないのかなとも考えられます。

さて、数年前に一生懸命検証をした時の1つの結論は、「ちゃんとドメインを切って、正しい証明書をセットアップしたサイト」にするということです。その時の設定はまだあって、IdP用にidp.inter-mediator.com、アプリケーションとSPはdemo.inter-mediator.com/saml-trialにしました。いずれも、Let’s Encryptではありますが、それぞれ有効な証明書が動き、通信はすべてHTTPSで動くという状態になっています。

IdPサイトの構築

IdPのサイトは、SimpleSAMLphpのコードをそのまま使って構築します。Ubuntuなので、/var/www以下に、例えば、以下ようなコマンドで、コードを取り出します。バージョンごとにタグがあるので、Ver.2系列の最新版である2.1.4をインストールすることにします。そして、composerを動かして、必要なライブラリのインストールを行います。

cd /var/www
git clone https://github.com/simplesamlphp/simplesamlphp simplesaml-idp
cd simplesaml-idp
git checkout v2.1.4
composer update

/var/www以下は、ログインしたユーザであれば書き込みできるという前提で説明をします。また、ログインしたユーザはsudoコマンド可能であって、root権限が必要な処理はsudoを利用するという方針でコマンドを示します。また、ログインユーザはadminsグループにも登録してあるものとします。

前述のコマンドで、/var/www/simplesaml-idpというディレクトリができ、そこにレポジトリの内容が展開されました。このディレクトリを公開するのかというと、そうではなくて、この中のpublicを公開します。以前はwwwというディレクトリでしたが、Ver.2でpublicという名前に変えたそうです。ということで、Apache2のidp.inter-mediator.comのサイト設定ファイルは、大体以下のような記述つまり、DocumentRootがある感じです(実際には証明書の設定などもあってもっとややこしい)。/simplesamlはIdPの設定ファイルに書かれているbaseurlpathの値でもあるので、とりあえずAliasを定義しておきます。

<VirtualHost *:443>
    ServerAdmin info@inter-mediator.org
    DocumentRoot /var/www/simplesaml-idp/public
    ServerName idp.inter-mediator.com
    Alias /simplesaml "/var/www/simplesaml-idp/public"
:

さて、サーバを見てみましょう!という感じで開くと、次の通りです。当然、セットアップを何もしていないので、そのような表示が出るだけです。ちゃんと、設定ファイルがないとメッセージが出ています。

IdPが使う証明書を用意する

SAMLでは通信の暗号化のために証明書を使います。IdPで使用する証明書は、opensslコマンドを使って作成しますが、レポジトリのcertディレクトリに作るのが一番手軽です。このディレクトリに作った証明書関連のファイルは、フルパスを指定する必要がありません。例えば、以下のようなコマンドで作成できます。

cd /var/www/simplesaml-idp/cert
openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes \
    -out idp.inter-mediator.com.crt -keyout idp.inter-mediator.com.pem

コマンド例ではカレントディレクトリを明示するためにcdコマンドを随所で書くようにしますが、もちろん、コマンドの理解がある方は自分の状況に応じてコマンドを入れてください。そして、opensslコマンドの-outと-keyoutの2つのパラメータは実際に保存されるファイル名になるので、自分のドメイン等に変えるか、server.cert、privatekey.pemみたいな名前にするのが良いでしょう。

乱数生成などの後、入力を促されます。要するに大雑把な住所と組織などを入力します。以下は私が入力した例ですが、もちろん、ご自分の状況に合わせてください。Common Nameについては、FQDNを入れるのが良いと思われます。

Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Saitama
Locality Name (eg, city) []:Midori-ward
Organization Name (eg, company) [Internet Widgits Pty Ltd]:INTER-Mediator
Organizational Unit Name (eg, section) []:Authentication Support
Common Name (e.g. server FQDN or YOUR name) []:idp.inter-mediator.com
Email Address []:nii@msyk.net

なお、生成されたキーファイルは、ownerだけが読み書きできて、gropuやeveryoneに対する読み出し権限すらありません。Apache2のプロセスのユーザ(Ubuntuではwww-data)が読み出し権限があるようにしなければなりません。しかしながら、アクセス権は、レポジトリの内容全体に設定した方が手軽でしょうから、アクセス権の設定は最後にまとめて行います。

configディレクトリの設定を行う

それでは、設定を進めましょう。まず、レポジトリのルートにあるconfigディレクトリの中身です。このファイルには3つの設定ファイルを作りますが、そのうち、config.phpとauthsources.phpの2つのファイルを用意します。このファイルはスクラッチから作るのではなく、ファイル名に.distが付いたテンプレートのファイルがあるので、それをコピーして用意します。まず、ファイルをコピーします。

cd /var/www/simplesaml-idp/config
cp authsources.php.dist authsources.php
cp config.php.dist config.php

config.phpファイルは、以下のポイントを修正します。 そのためにvimやnanoなどのエディタで開くことになりますが、その前に、以下のコマンドを入れて、secretsaltキーの値を生成しておきます。このことはファイルのコメントにも書かれてあり、以下のコマンドで生成して、出力結果をコピーしておきます。

LC_ALL=C tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' </dev/urandom | dd bs=32 count=1 2>/dev/null;echo

そして、config.phpファイルを編集します。まず、technicalcontact*は、このサーバの管理者です。基本的には自分を指定すれば良いでしょう。secretsaltはファイルを開く前にコピーしたものを指定すればよく、文字列の中身を消してペーストします。auth.adminpasswordは、IdPのログインする管理者のパスワードです。

:
    'technicalcontact_name' => 'Administrator',
    'technicalcontact_email' => 'msyk@msyk.net',
:
    'secretsalt' => 'whr5p645s3ig7nm9wxibfckllmjfvjl6',
:
    'auth.adminpassword' => 'samltest5682',
:
    'enable.saml20-idp' => true,
    'enable.adfs-idp' => false,
:
    'module.enable' => [
        'exampleauth' => true,
        'core' => true,
        'admin' => true,
        'saml' => true
    ],

enable.saml20-idpは、文字通り、IdPの機能をアクティブにします。module.enableは、exampleauthの値をtrueにしますが、これは、設定ファイルで認証ユーザを提供する仕組みをオンにします。もちろん、簡易的にテストができるようにということです。

続いて、config/authsources.phpの修正です。まず、default-sp以下の配列において、entityIDを変更します。そして、この配列内に、privatekeyとcerificateというキーで、それぞれ秘密鍵と証明書のファイル名を指定しておきます。もちろん、ここでは、前の手順でopensslで生成したファイルを指定します。さらに、テスト用のユーザとして、example-userpassの部分のコメントを外して、その中に定義します。以下の例では、user01というユーザとuser02というユーザが定義されており、それぞれ、パスワードはuser01pass、user02passです。キーになっている’user01:user01pass’の部分でユーザ名とパスワードを表現しており、対応する配列は応答する情報を記載します。ちなみに、大学のディレクトリなどでは、eduPersonAffiliationといった属性が入ってきて、それに応じて大学生か、職員かを判断するようなロジックを求められることはよくあるようです。

:
    'default-sp' => [
        'saml:SP',

        // The entity ID of this SP.
        'entityID' => 'https://idp.inter-mediator.com/',
:
        'proxymode.passAuthnContextClassRef' => false,

        'privatekey' => 'idp.inter-mediator.com.pem',
        'certificate' => 'idp.inter-mediator.com.crt',
:
    'example-userpass' => [
        'exampleauth:UserPass',
:
        'user01:user01pass' => [
            'uid' => ['user01'],
            'eduPersonAffiliation' => ['member', 'student'],
        ],
        'user02:user02pass' => [
            'uid' => ['user02'],
            'eduPersonAffiliation' => ['member', 'employee'],
        ],
    ],

metadataディレクトリの設定を行う

続いて、レポジトリルートにあるmetadataディレクトリの設定を行います。このディレクトリも設定ファイルはないものの、拡張子が.distとなっているそれぞれのファイルのテンプレートがあるので、それをコピーして変更して利用します。3つのファイルがありますが、利用するのは2つだけです。コピーしないsaml20-idp-remote.phpファイルは、SPで利用するものです。

cd /var/www/simplesaml-idp/metadata
cp saml20-idp-hosted.php.dist saml20-idp-hosted.php
cp saml20-sp-remote.php.dist saml20-sp-remote.php

ちなみに、ファイル名がややこしいと思われるかもしれませんが、それぞれ、IdPの設定、SPの設定を行います。IdP自分自身についてはhostedの方で設定します。そして、SPの設定は自分ではないので、remoteであるということです。ファイル名にはきちんと意味があると思えば、少しは見通しよく見えるのではないでしょうか。

metadata/saml20-idp-hosted.phpについては、以下を修正します。まず、$metadata配列のキーについてはキーの値を既定値から変更して設定します。ここでは、とりあえず、IdPのドメインにしました。ちなみに、このキーを既定値のままにすると、動作がおかしかったので、これを切り替えるのが必要ではないかと思われます。そして、privatekeyとcertificateキーのファイル名を、生成したファイルのものに切り替えておきます。

$metadata['https://idp.inter-mediator.com/'] = [
    /*
     * The hostname of the server (VHOST) that will use this SAML entity.
     *
     * Can be '__DEFAULT__', to use this entry by default.
     */
    'host' => '__DEFAULT__',

    // X.509 key and certificate. Relative to the cert directory.
    'privatekey' => 'idp.inter-mediator.com.pem',
    'certificate' => 'idp.inter-mediator.com.crt',

実際にはもっといろいろ変更は必要なのでしょうけど、ここまでの設定だと、証明書やキーのファイルの整合、IdPを稼働、テストユーザの登録程度のことです。

全てのファイルの所有者とグループを揃える

必要なファイルをすべて揃えたので、simplesamlphpのファイルの所有者を、Webサーバのwww-dataに変更しておくのがいいように思います。例えば、次のようなコマンドです。

sudo chown -R www-data:admins /var/www/simplesaml-idp
sudo chmod -R g+w /var/www/simplesaml-idp

こうすれば、simplesaml-idp以下のすべてのファイルやフォルダは、所有者がWebサーバのプロセスのユーザであるwww-dataになり、グループはadminsになります。そして、所有者はrwあるいはrwxになりますが、グループも同様なアクセス権になることを期待します。通常ログインするユーザをadminsグループに入れておけば、そのユーザでのファイルの読み書き権限もあり、Webユーザの読み書き権限も確保していると言うことになります。simplesamlphpのIdPでは、ファイルの書き込み権限がWebサーバに対して必要なのかという問題はありますが、とりあえずはメンテナンスしやすい状態にしていると考えてください。

キャッシュのディレクトリを用意する

ここで、https://idp.inter-mediator.com/ つまり、Webのルートにアクセスすると次のような画面が出てきます。Ver.2.0.xではこのような画面は出てこなかったのですが、Ver.2.1.xでは出るようになったようです。

このエラーはよく読むと、意味がわかります。どうやら、既定値では、/var/cache/simplesamlphp以下のキャッシュファイルを作るようで、そのディレクトリが必要ということに加えて、アクセス権も設定が必要なようです。例えば、次のようなコマンドで対処できます。

sudo mkdir -p /var/cache/simplesamlphp
sudo chown -R www-data:admins /var/cache/simplesamlphp

キャッシュとして、かなりたくさんのファイルが作られます。

なお、simplesamlphp自体をgitを使って更新した後などは、場合によってはキャッシュをクリアしておかないと起動時にエラーになる場合もあります。エラーにならない時もあるのですが、いずれにしてもソースコードの変更によってキャッシュの整理は場合によっては自分でやらないといけない模様です。謎のエラーが出た場合には、/var/cache/simplesamlphp以下を消してみてください。

管理ツールを稼働する

ここで、https://idp.inter-mediator.com/ つまり、Webのルートにアクセスすると次のような画面が出てきます。ちゃんと動いている模様ですが、肝心の管理作業ができません。

管理作業をするには、https://idp.inter-mediator.com/admin にアクセスします。いろいろリダイレクトしますが、認証画面が出てきます。ここでは、ユーザ名はadmin、パスワードは、config.phpファイルに指定したパスワードを入力して認証します。

最初は、以下のようにTestというタブのページになります。ここから先は次の記事で説明ます。

Ubuntu 22でINTER-Mediatorを稼働する

Ubuntu Server 22.04.1 LTS上で、INTER-Mediatorのサンプルを、MySQLで動かすところまでのセットアップ方法を紹介します。サーバは普通にDVD等でインストールします。ほぼ、デフォルトでセットアップした状態を想定しているので、Minimalの方ではありません。また、サーバアプリケーションは、SSH Serverだけをセットアップ時に含めているとします。

ということで、早速、インストール後のコマンド入力です。一気にまとめて紹介します。

sudo apt -y update
sudo apt -y upgrade
sudo apt install -y apache2 php mysql-server
sudo apt install -y php-curl php-xml php-gd libicu-dev \
                    mysql-client php-pdo-mysql
sudo apt install -y nodejs
sudo apt install -y composer
sudo chmod -R g+w /var/www
sudo chown -R www-data:<user> /var/www
sudo systemctl restart apache2

cd /var/www/html
git clone https://github.com/INTER-Mediator/INTER-Mediator.git
cd INTER-Mediator/
composer update
cd dist-docs
sudo mysql -uroot < sample_schema_mysql.sql 

「php」でインストールすると、Ver.8.1がセットアップされます。モジュール類も以前よりも多く初期設定で入っているので、記載した、php-curlなど3つと、データベースのドライバを追加するだけで大丈夫です。ただ、intlモジュールが利用するlibicu-devを入れておかないといけないのは以前から変わっていないところです。php-pdo-mysqlは実は存在しておらず、php8.1-mysqlが代わりにインストールされます。php-mysqlというモジュールもあってこちらでも良さそうな気がしますが、とりあえず、PDO本体は入るけどもMySQLのPDOサポート部分は追加しないといけないというところがポイントです。よって、PostgreSQL等でも同様にPDOドライバを入れないといけないということです。

Node.jsは「念の為に」入れておきます。composerもaptでインストールできるようになっています。

Apache2は以前の通り、www-dataユーザで稼働するので、このユーザのホームである/var/wwwのアクセス権を設定しておきますが、chownでのグループはログインユーザ名にしておくのがいいかと思います。そして、Apache2を再起動します。以前よりだいぶんとシンプルになった気がします。

後半は、INTER-Mediatorのインストールです。とりあえず、Web公開ディレクトリにレポジトリの中身を展開してそれを動かすことにします。クローン後、composer updateコマンドを動かし、サンプルのデータベースをMySQLに読み込ませて準備するだけです。これで、「http://ホストIP/INTER-Mediator/samples/」で、サンプルの目次ページが出てくるはずです。

現在は既定値でサービスサーバを落としていますが、INTER-Mediator/params.phpの以下の部分を修正すると、サービスサーバが稼働します。コード部分は修正前ですので、コメントに従って変更してみてください。Sample_formフォルダのサンプルがクライアント間同期の仕組みを組み込んであります。サンプルの目次ページだと、「Any Kinds of Samples」の最初にある「Master-Detail Style Page」のリンクを利用してください。

$notUseServiceServer = true; // 値をfalseにする
/*  // この行を消してコメントでなくする
$activateClientService = false; // 値をtrueにする
$serviceServerProtocol = "ws";
$serviceServerHost = "";
$serviceServerPort = "11478";
$serviceServerKey = "";
$serviceServerCert = "";
$serviceServerCA = "";
$serviceServerConnect = "http://localhost"; // localhostを実際のホストにする
$stopSSEveryQuit = false;
$bootWithInstalledNode = false;
$preventSSAutoBoot = false;
$foreverLog = '/tmp/forever.log';
*/ // この行を消してコメントでなくする

JavaScriptのasync/awaitを再理解してみた

半年ぶりの投稿になりますが、表題のことをやってみました。ちょっと時間の余裕ができたので、改めて勉強しようとしていろんなサイトを見たのですが、「async関数はPromiseを返す」ということが前提だけに、まずはPromiseを勉強するという体裁になっています。一方、「async/awaitは結局のところ、非同期呼び出しを同期呼び出しのように書いてよし」というシンプルに考えていいのかどうか、非常に迷うところでした。ということで、忙しい世の中ですので先に言ってしまうと、そこまでシンプルには残念ながら行かないことがわかります。INTER-Mediator Ver.6の実装で結構苦労したことが、本当にその苦労が必要だったのかということの検証でもあるのですが、これは個人的なところですね。

まず、次のようなプログラムがあるとします。以下は、適当な.jsファイルに入れて、nodeコマンドで実行します。ただし、「npm install xmlhttprequest」あるいはsudo付きで、XMLHttpRequestモジュールのインストールをお忘れなく。ブラウザで適当に動くようにされているのなら不要です。単にWebサイトのデータをAJAXで取り出すプログラムですが、どんな順序で実行されたかわかるように、コンソールへ出力を残しています。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const r = new XMLHttpRequest();
  r.open('GET', 'https://msyk.net')
  r.onreadystatechange = () => {
    if(r.readyState == 4) {
      console.log('receive aFunc');
    }
  }
  console.log('setup aFunc');
  r.send()
  console.log('send aFunc');
}

console.log('before aFunc');
let a = aFunc(); // async関数を普通に呼び出してみた
a.then(console.log('act1')) // thenが呼び出せるということはPromiseが返っている
console.log('after aFunc');

これを実行すると、次のようにコンソールに出てきます。ポイントは「after aFunc」より後に「recieve aFunc」が出るということです。AJAXによるダウンロード処理は非同期で行われているので、関数aFunc内に記述したコードが実行し終わる前にaFuncを呼び出しているステートメントの次に行くのです。これが非同期呼び出しの動作です。JavaScriptは「プログラムとして書いたコード」は、並列では動きませんが、「書いた順序」では実行しないというよくあるプログラムです。thenについてはPromiseを勉強すれば必ず説明されていますので、省略します。

before aFunc
start aFunc
setup aFunc
send aFunc
act1
after aFunc
receive aFunc

さて、ここで、やりたいことは「通信が終わったら次に行く」と言う動作です。要するに、「aFuncを同期的に呼び出したいのでしょ。だったら、awaitを入れればいいじゃん」と思うところですね。やってみましょう。ここで、単にaFunc()の前にawaitをつけると文法エラーになります。awaitはasync関数内部でしか使えません。グローバルエリアはasyncではないので、エラーになるので、新たにbFunc関数を作り、そこで実行します。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const r = new XMLHttpRequest();
  r.open('GET', 'https://msyk.net')
  r.onreadystatechange = () => {
    if(r.readyState == 4) {
      console.log('receive aFunc');
    }
  }
  console.log('setup aFunc');
  r.send()
  console.log('send aFunc');
}

async function bFunc() {
  console.log('before aFunc');
  let a = await aFunc(); // async関数を普通に呼び出してみた
  //a.then(console.log('act1')) // thenはエラーになり呼び出せない
  console.log('after aFunc');
}

bFunc()

おっと、thenの呼び出しでエラーになるので、コメントします。そして、コンソールの出力結果を見ると、次のようになっていました。前と変わりません。after aFuncより後にreceive aFuncがあるということは、aFuncのコールが終わってから実行したコードよりも後に、aFuncの中身が処理されています。つまり、async/awaitを使っても同期呼び出しされていないということです。おやおや、おかしいですね。

before aFunc
start aFunc
setup aFunc
send aFunc
after aFunc
receive aFunc

ここで、thenの呼び出しでエラーを出してみると、This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().などと書かれています。はい、ここでPromiseの知識が必要です。Promiseでは、thenやcatchメソッドを呼び出すことで、「先に進める」的な動作をするというか、それを期待しているとも言えます。thenメソッドの2つの引数にresolveやrejectメソッド(もちろん、その名前でなくてもいいのですが、たぶん、皆さんは常にそう書いていると思います)が乗ってくるので、そちらの方がお馴染みかもしれません。ともかく、Promiseを書かないで、asyncにしたからと言って、そのままawaitできるのかというと、この例で示すようにできないということになります。

では、どう書けばいいかというと、こういうことです。非同期処理部分をPromiseで包みます。既存の処理をPromise化するのに適した方法として、コンストラクターの引数に、処理を記述する方法です。処理が終わったらresolve()を呼び出します。reject()は省略します。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const p = new Promise((resolve, reject) => {
    const r = new XMLHttpRequest();
    r.open('GET', 'https://msyk.net')
    r.onreadystatechange = () => {
      if(r.readyState == 4) {
        console.log('receive aFunc');
        resolve()
        console.log('resolve aFunc');
      }
    }
    console.log('setup aFunc');
    r.send()
    console.log('send aFunc');
  })
}

async function bFunc() {
  console.log('start bFunc');
  await aFunc()
  console.log('end bFunc');
}

bFunc()

結果はこのようになります。おや?receive aFuncが、end bFuncより後にあります。これじゃあ、全然同期的に呼び出しているんじゃないですね。なんででしょうね?どこが間違っているのでしょう? これは難しいクイズですね。実はブログを執筆しながら、偶然発見してしまったトラブルです。

start bFunc
start aFunc
setup aFunc
send aFunc
end bFunc
receive aFunc
resolve aFunc

はい、答えは、aFunc関数の最後に「return p」つまり、生成したPromiseを返すというステートメントが必要になります。async内にPromise生成を記述しないと、自動的に生成されるけど、書いてしまったら、ちゃんと自分で返すようにしないといけないと言うことですね。そうすれば、コンソールには次のように出力されます。無事、非同期通信処理の結果を受け取ってから、await aFunc()の次のステートメントを実行しています。つまり、aFunc()は非同期処理が記述されているけども関数呼び出しは同期的に処理が行われたと言えます。

start bFunc
start aFunc
setup aFunc
send aFunc
receive aFunc
resolve aFunc
end bFunc

ところで、前のプログラムで、bFunc()の部分の次にconsoleを記述したら、どの順序で実行されるでしょう。一番最後? それは違いますね。receive aFuncの前にbFunc()の次に記述したステートメントが実行されます。つまり、bFunc()によって呼び出されるのは非同期関数なのです。当たり前ですね、asyncと頭に書いているから。なので、さらにbFunc()呼び出しの部分を同期的にやりたいとなると、ちょっとしつこいですが、こんな風に書かないといけません。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const p = new Promise((resolve, reject) => {
    const r = new XMLHttpRequest();
    r.open('GET', 'https://msyk.net')
    r.onreadystatechange = () => {
      if(r.readyState == 4) {
        console.log('receive aFunc');
        resolve()
        console.log('resolve aFunc');
      }
    }
    console.log('setup aFunc');
    r.send()
    console.log('send aFunc');
  })
  return p
}

async function bFunc() {
  console.log('start bFunc');
  await aFunc()
  console.log('end bFunc');
}

async function cFunc() {
  console.log('# before bFunc');
  await bFunc()
  console.log('# after bFunc');
}

cFunc()

コンソールへの出力結果は次のようになり、期待通り、awaitで非同期処理が同期処理として呼び出されています。

# before bFunc
start bFunc
start aFunc
setup aFunc
send aFunc
receive aFunc
resolve aFunc
end bFunc
# after bFunc

最終的に、ここでは、cFunc()という非同期呼び出しがありますが、他の部分のコードは全部、同期的に動いていると言える状態かと思います。ですが、全部の関数にasyncがつけられていて、非同期に動く関数であることを明確に示しています。INTER-Mediatorでの実装では、通信処理は何段階か呼び出した低いレイヤーで行なっています。そこをasync/await対応にしたら、その1つ上をやっぱりasync/await対応にしてということになり、要するに全階層がasync/await対応、つまり、全レイヤーが非同期処理するという状況にまずはなりました。asyncで非同期にして、awaitで同期的に呼び出すと言うことをあちらこちらに適用しないといけなくなりました。なんだか、非同期処理を同期的に記述してプログラムしやすくするという目的があるのに、全体的に非同期処理になってしまうではないかというジレンマに陥りました。結局、全体的に見直しが必要になったのは言うまでもありませんが、AJAXを非同期にするために、なんとか頑張ったのでした。実は、それから数年経過し、その頑張りに意味があったのか、ちょっと検証したくなったので、今日、こんなテストをやっているうちに、この記事を書こうと考えた次第です。まあ、方針としては間違いなかったのかと思います。お付き合いありがとうございます。

[続開発プロセス#14] エンクロージャー/リピーターの制約

前回の記事で、HTMLのテンプレートの設計、つまりページファイルの設計を出すときに、非常に根深いところにある制約の話が必要であることに気付きました。ですが、前の記事が長いだけに、別途記述するということで、連投となりました。

話を少しすっ飛ばしたのは、ページのモックアップにある左側の、カテゴリ一覧の設計です。まず、オブジェクト指向的に正しくモデリングをするということを進めます。以下の図は、ともかく見えるオブジェクトとをクラスとして記述して、全体をまとめるであろうCategoryBox配下に並べたところです。オレンジ色のメモに記述したように、ここには矛盾があります。ここでは、「小分類は必ず何かの大分類に所属している」前提があるとしているので、このように、大分類に所属しない小分類が存在しえるモデルは、モデル自体が間違っているので、正しい設計とは言えません。

ここで、小分類は大分類に所属することをモデルとして表現してみます。小分類の名前とボタンがあり、それぞれ、MinorCategoryLabel、MinorCategoryButtonクラスですが、既に存在する大分類の名前であるMajorCategoryLabelに関連づけてみました。これだと、小分類の存在は大分類の存在を前提になっている状況は示しています。しかしながら、オレンジ色のメモに記述したように、小分類の中でラベルとボタンがバラバラであり、ラベルが4つでボタン3つでも成り立ちそうなモデルです。ここで、1つの小分類は、何らかの1つの対象が保持する形にであるべきではないかと考えます。

そこで、次のように、小分類の項目をまとめるMinorCaterogyBoxクラスを間に入れてみました。この辺りで分類そのものをデータベースのテーブルに入れていることを考えれば、MinorCategoryLabelなどフィールドに相当するものが、レコードに相当するMinorCategoryBoxにまとめられて、それが1つの大分類に所属するという状況をだいぶんと正確に示してきていることが伺えます。ここで、改めて大分類に目を向けます。ここでの大分類の名前を示すMajorCategoryLabelは大分類の1項目の中の1つのフィールドであり、属性の1つにすぎません。これ自体が大分類項目1つを代表するのはちょっと無理があるのではと考えられますし、小分類の類するから、「大分類の1つの項目をまとめるものがあれば良い」ことが考えられます。

そして、次の図のように、大分類の1レコードに対応するMajorCategoryBoxが存在し、大分類の1つのフィールドは、このMajorCategoryBoxから1対1で表現しています。また、1つの大分類から複数の小分類が関連づけられることも、MajorCategoryBoxとMinorCategoryBoxの1対多の関係として表現できています。ここで、エンクロージャー/リピーターの関係を1対多の関係に持ち込み、データベースのデータをバインドしたいと考えます。これはINTER-Mediatorの肝になる機能です。

ここで改めて、エンクロージャー/リピーターをモデルとして示します。HTMLのテンプレートは何がクラスで何がオブジェクトかというのは視点あるいは場面によって変わると思われますが、HTMLの記述可能な範囲において、エンクロージャーがリピーターを含み、リピーターにはターゲットノードが含まれる階層構造になっています。オブジェクト図で記述すると明白なように、これらはクエリー結果のリレーション/レコード/フィールドと言った階層に一致しています。

INTER-MediatorはDOMの領域でテンプレート処理を行っています。テキストのレベルで行うより高い粒度になりますが、一方でそのための制約が発生しています。このリレーションとテンプレートを合成するアルゴリズムでは、テンプレートにあるリピーターを一度複製を取って削除します。複製は所属するノードも含めるので、複製はテンプレートそのままにターゲットノードを含めて保存されています。そして、リレーションの中にレコードがあればその保存したリピーターを複製して、エンクロージャーの子要素とします。この作業を繰り返します。この流れによってリピーターがレコードの数だけ繰り返されて、一覧表示が完成します。

このとき、必須ではないのですが、エンクロージャーの子要素は全てリピーターである方が最終結果は予測付きやすいです。仮にエンクロージャーにリピーターでない子要素がある場合、現状は結果的にリピーターが繰り返す前に集まってしまいます。それでいいのかもしれませんが、そうなら、エンクロージャーの前に記述すればいいことなので、リピーター以外はエンクロージャーの子要素にはしないというルールは問題ありません。ところが、前要素→リピーター→後要素のように並んでいた場合、気持ちは前要素→展開したリピーター→後要素のようになって欲しいかもしれませんが、そうなりません。そこで、header、footerとなるリピーターやseparatorとなるリピーターも定義しています。とにかく、テンプレートで並べたリピーター以外の要素が展開後どうなるかを意識しなくてもいいように、リピーター以外は子要素にないのが良いと考えます。これは、INTER-Mediatorの大きな制約なります。

ここで、エンクロージャーとリピーターの展開は、1対多の箇所に対応づけられるのがわかったので、以下の図のように、ステレオタイプで、enclosure/repeater/tareget nodeを割り当てました。しかしながら、ここで、MinorCategoryBoxがリピーターなのですから、その上位であるMajorCategoryBoxがエンクロージャーになる必要があります。もちろん、そうすると、MajorCategoryBoxはエンクロージャー件、さらに上位のコンテキストに対するリピーターでもあります。この状態でINTER-Mediatorは稼働するのではありますが、こうなると、MajorCategoryBoxの子要素に、MinorCategoryBoxとMajorCategoryLabelの2つの要素が割り当てられます。前者はリピーターですが、後者はリピーターではなく、INTER-Mediatorの制約に反します。そこでどうするかを考えないといけません。

ここで、新たに、MinorCategoryConteinerクラスを導入します。つまり、レコードの数に応じて複数存在するMinorCategoryBoxクラスのオブジェクトをまとめる存在のものを用意します。HTMLではこの記述は、汎用タグのdivやspanで簡単にできます。すると、MajorCategoryBoxはMajorCategoryLabelとMinorCategoryContainerを含み、リピーターを含まないことになります。(なお、AllSelectButtonは本来はエンクロージャーであるCategoryBoxの外に出すべきです。この要素は、リピーターではないからです。)

このような、エンクロージャーとリピーターの間の制約があり、そのために、ここでは4段階のdivタグで囲まれるようなHTMLコード例を示したわけです。

この制約は一般には考えにくい事実かもしれません。HTMLは独特の柔軟性を持っていることを利用しているということと、やはりデータベースの検索結果であるリレーションという表形式のデータを自動的に展開するという仕組みが微妙にコンフリクトする場面でもあります。リレーションの結果を調整するのはもちろんですが、一方で、HTMLのテンプレートも、リレーションをマッピングできるように調整しないといけないということになります。

元々、INTER-Mediatorは、trタグの要素がtbodyタグ要素の子要素として繰り返すことで、レコードの展開を「繰り返し」の拡張命令的な処理をしなくても実現できることを発見したのが始まりです。tbodyタグの子要素にはtrタグ要素しか登録できません。それ以外のものはテーブルの外にはみ出ますし、定義上、trしか存在できないはずです。また、同時にselectとoptionについても同様な関係があるとして、これらの2組は自動的にエンクロージャー/リピータとして認識するようにしました。前述のカテゴリ一覧も、tableの中にtableを作る方法でできると説明してしまえが一言で終わってしまい、あとは試行錯誤してコンテキスト定義を行えば動くでしょう。しかしながら、data-im-control=”enclosure” / data-im-control=”repeater”により、どんなタグでもエンクロージャーやリピーターになりうるようにしたのですが、その結果、明示的にエンクロージャーとリピーターのタグを記述しないといけなくなってしまっています。デザイン的には不要でも、INTER-Mediatorのアルゴリズムに合わせるために、ここでのMinorCategoryContainerのような要素が必要になります。

[続開発プロセス#13] INTER-Mediatorによるアプリケーション

制約のある状況での開発プロセスについて、MVCフレームワーク、FileMakerでそれぞれ簡単ですが、設計をやってみました。そして、本題であるINTER-Mediatorでの設計を今回は説明します。

まず、以下の図は、共通の設計モデルを示すクラス図に、INTER-Mediatorで考慮すべきことをメモとして追加したものです。まず、CategoryModelに紫色のメモで示したように、カテゴリーの全リストはINTER-Mediatorでは不要なので、ここメソッドを削除しています。また、PickingUpクラスやMemoListクラスの属性は、結果的にデータベースのテーブルにあるフィールドが必要になるので、ここではどんなデータがUIのレイヤーに来るのかを明示するため、属性にフィールド名を追加しました。

まず、前の図の下の方にあるステレオタイプがtableの部分をみてください。これは、ドメインモデリングの作業で作ったものと同じであり、もちろん、このままデータベースのスキーマとして定義します。アクセス権の設定なども行、データベースの準備は進めておきます。そして、ステレオタイプがmodelのものは、「データベースから何が抽出されたいか」を操作で示しています。これらの機能を、この後の定義ファイルの設計において、コンテキストとして取り出せるものを用意する段階で改めて調べます。

そして、前の図の上半分に記述されたUIレイヤーについて検討を進めます。まず、UI Componentステレオタイプの2種類のものは、INTER-Mediatorでは実装された機能やプラグインとして用意されているので、それらを呼び出すだけでOKです。ここは機能そのものやプラグインの制限が影響なければ特段の配慮は通常は不要です。

前の図で、緑色に塗ったクラスは、結果的にはある段階で、エンクロージャーとして展開可能なものと対応づけるようにします。INTER-Mediatorでの設計のポイントは、エンクロージャーとリピーターの特定、つまり、どこでこれらの要素を出すのかによります。ここでは設計段階なので、まずは、データベースから得られる1つのリレーションに対して、UIレイヤーでは1つのクラスとして認識するところまでを進めます。つまり、メモの一覧がデータベースにアクセスしてリレーションとして得られます。リレーションなので複数のレコードですが、そのレコードには、緑色のクラスの属性にあるようなフィールドが存在するはずです。その意味で、緑色のクラスがリレーションかあるいはエンクロージャーを示すのであれば、その属性というのは途中をすっ飛ばした不正確な記述になるかと思いますが、ここでは勘弁性を優先してこのように記述します。この記述については、定義ファイルの設計でも同様な考慮をします。

MemoListクラスは、データベースから得られたメモのリストを展開するという意味ではわかりやすいでしょうけども、PickingUpは、大分類と小分類が入り乱れます。一応、1つのクラスPickingUpとして記述しましたが、それは正しいでしょうか? ですが、まずはカテゴリを取り出してボタンと一緒に表示するという仕組みが必要であることを表明するためとしてPickingUpクラスの存在は無駄なものではありません。しかしながら、大分類/小分類というかなりややこしい状況があります。

ここから、HTMLのテンプレートを実際に作ります。もちろん、これはINTER-Mediator用語で言えばページファイルを作成に入ります。ここで解決すべきことは、エンクロージャーとリピーターの特定になります。もちろん、要素の取り出しも重要ですが、INTER-Mediatorではこの「エンクロージャーとリピーターの特定」さえできれば、モデリングは終わったも同然だと考えられます。ただ、今回は設計プロセスを追うのが趣旨なので、詳細に記述します。

エンクロージャーとリピーターの特定に必要なことは、得られるリレーション間の関連です、つまり、リレーションシップの特定に他なりません。まず、前のシステムデザインで記述されたMemoListについては、メモが複数あるので、リスト全体とメモ単体の間に1対多の関係があり、ここで1に対してエンクロージャー、多に対してリピータを割り当てれば良いので、ここでのエンクロージャーとリピーターの識別は容易です。なお、結果的に1:*になる部分は*の数が一定しない、つまりデータに応じて変わる部分であり、結果的にデータベースからのリレーションを展開することになるでしょう。ボタンが10個並ぶなら、1:10になるので、これらはスタティックなものとも言えます。つまり、HTMLのテンプレートをモデリングした場合に出てきた1:*の部分がエンクロージャーとリピーターとして記述すべき箇所になるということが言えます。

しかしながら、ここで、ステレオタイプがtableの部分をみてください。memoテーブルのtop_category_id、sub_category_idは、それぞれcategoryテーブルのcategory_idと対応づけていて、ここで各メモがどのカテゴリーに分類されているのかを記録できるようになっています。また、category同士のリレーションシップも設定されており、これにより、大分類と小分類のレコードの関連づけが行われています。このようなスキーマにみられるリレーションシップは、エンクロージャーとリピーターの内部にさらにエンクロージャーとリピーターが存在することを示唆します。そのことを考慮して、ページファイルの中心部分をモデル化したものは以下の通りです。

まず、HTMLテンプレートの設計の上部を説明しましょう。ここは、システム設計でのPickingUpの部分です。ここでは、大分類と小分類が一覧されているという見え方になっていますが、もう少し詳細に見ると、1つの大分類に対するその小分類が複数リストになっています。つまり、大分類と小分類が1対多の関係になっているものが、さらに、大分類の数だけ並んでいるということになり、全体から見れば、多数の大分類があり、さらに、その大分類に対応した多数の小分類があるという階層構造になっています。これらは、ネストしたエンクロージャー/リピーターで定義します。外側のエンクロージャー/リピーターは次のように定義するとします。class属性は、HTMLテンプレートでのクラス名をつけておきます。

<div data-im-control="enclosure" class="CategoryBox">
    <div data-im-control="repeater" class="MajorCategoryBox">
        <div data-im="****@category_name"></div>
    </div>
</div>

このリピーターは、大分類の項目1つに対応します。なので、リピーターの中でcategory_nameフィールドを表示すると、このエンクロージャーに割り当てるコンテキストが大分類の一覧にしておくことで、ページ上には大分類だけの一覧が表示されます。コンテキスト名はまだ決めていないので、****にしておきます。

そして、このリピーターの中に、対応する小分類の一覧を表示するエンクロージャー/リピーターを定義します。次のようになります。

<div data-im-control="enclosure" class="CategoryBox">
    <div data-im-control="repeater" class="MajorCategoryBox">
        <div data-im="****@category_name"></div>
        <div data-im-control="enclosure" class="MinorCategoryContainer">
            <div data-im-control="repeater" class="MinorCategoryBox">
                <button onclick="****">選択</button>
                <div data-im="****@category_name"></div>
            </div>
        </div>
    </div>
</div>

ここで、内側のエンクロージャー/リピーターの表示を行うためにリレーションの取得を行う場合、その時の外側のリピーターで展開している現在のレコードと照合する、つまりリレーションシップを考慮することが、定義ファイルでの設定で可能です。その仕組みを利用して、内側のエンクロージャー/リピーターでは特定の大分類に対する小分類だけが表示されるようになります。

引き続いてMemoLineのリピーターから続く2つのエンクロージャーについて説明します。ここではまず、memoテーブルだけだと、大分類や小分類は主キー値を記録していることで、どの分類なのかは記録されていますが、単なる数字なので、人間が目で見て判別するのは困難ということがあります。つまり、memoテーブルの値に加えて、選択した分類の名前を知りたい分けです。名前が別テーブルにあるので、つまりはそこでリレーションシップに応じたテーブル連結をするのがSQLでの定番ですが、UIレイヤーではそういう用途にselectタグによりポップアップメニューが使われてます。ポップアップメニューは、ここでは、top_category_id値を対応させることができますが、そのcategory_idに対応する値を内部で記録しつつ、ページ上には別の文字列を表示することもできます。この場合、categoryとmemoが1対多の関係になりますが、逆にメモの一覧にある大分類のポップアップメニューには、選択された大分類項目だけでなく、選択肢として表示されるように全ての大分類の項目が必要であり、それぞれoptionタグでselectタグの内部に記述しなければなりません。つまり、selectは選択肢の提供と、選択したものの名称表記の両方の機能を持っているのです。逆に言えば、selectを適切に使えば、選択肢の用意だけで選択したものの名前はそこにあるものを使うので、一石二鳥であるとも言えます。INTER-Mediatorでは、selectとoptionの関係をエンクロージャー/リピーターとして扱うので、その関係をモデル図の右下のあたりに2箇所記述しました。

INTER-Mediatorで一番難しいのは、このエンクロージャー/リピーターの特定、そして、その階層関係での定義ではないかと思われます。エンクロージャー/リピーターを特定するには、そこにリレーションが得られるという事実と関連づければ良いのですが、階層関係になると非常に難しくなるというのはあるでしょう。やはり、動作原理を知るのがしっかりとした設計を行うためには必要なことは言うまでもありません。

ここまでのところで5つのエンクロージャー/リピーターが出てきました。よって5つのコンテキスト定義が必要という見方もできます。ここで、コンテキストを実際に定義して行く作業に入ります。実際には4つの定義で問題ないことがわかります。

ここで、コンテキスト定義は、以下の図のように、Contextクラスを元にインスタンス化したオブジェクトとして記述することにします。現実には連想配列なのでコンテキスト定義は言語の意味でのクラスを元にはしていませんが、認識可能なキーが決まっているのは、属性が定義されていることと同義ですので、この点は問題ないと思われます。実際には、コンテキスト定義を利用して、コンテキストオブジェクトが生成されます。正確に記載すると以下の図の左側、Precise wayと書かれた方のクラス図になります。つまり、コンテキストオブジェクトConcreteContextObjectはテーブルConcreteTableを継承しているか、あるいは参照することになり、その結果、ConcreteEntityの配列がstore属性から参照できるようになっています。ここで、ConcreteContextObjectをインスタンス化した時、当然ながら継承元のフィールドがそのまま所有します。ここで、テンプレートバインディングやあれこれとUMLの仕組みを試したのですが、モデルとしてしっくりくるものがありません。そこで、右側のように、Concise wayと書かれたように、コンテキストで利用する実テーブルであるTableを継承して、ContextObjectを定義します。名前は任意にして、このような目的のクラスであることをステレオタイプのcontext objectで示すことにします。こうすれば、右下のようなオブジェクトが記述でき、テーブルのフィールドをそのまま持ちます。もちろん、この表現は不正確であり、コンテキストオブジェクトはTableによるレコードの配列を持つのではありますが、そこを正確に全部記述すると図が煩雑になるだけなので、「実際にはそういうことだけども勘弁性を重視してこのように書く」という合意ができたとして、Concise wayのような表記をします。

ここでHTMLテンプレートの設計を見ながら、コンテキスト定義のオブジェクトを検討します。ここでは代表的な属性だけを記述することにします。結果をまとめると以下の図になります。

まず、カテゴリの一覧部分、つまりHTMLテンプレートの設計でいえば、CategoryBoxクラスとMinorCategoryContainerクラスを取り出します。これらはエンクロージャーなので、リレーションと対応させないといけません。ここでは図で示せるように、tableステレオタイプのcategoryクラスを継承して、top_category_listとsub_category_listのコンテキストオブジェクトクラスを定義し、それらのインスタンスとして、緑色のボックスのオブジェクトを記述しましたが、これは実際にここまでの記述は不要でしょう。記述すべきは山吹色のボックスのコンテキスト定義のオブジェクトのみです。コンテキスト定義のtop_category_listでは、viewあるいはtable属性を見ることで、リレーションの元になっているテーブルが判別できるので、結果的に薄黄色と緑色のボックスは、コンテキスト定義の情報や、スキーマの情報からすぐに入手可能であるということです。

同様にして、memo_list、minor_category_list、major_category_listのコンテキストを定義します。memoテーブル由来のコンテキストはmemo_listのみですが、残りの4つのコンテキストはcategoryテーブルを由来としています。このように、同一テーブルからであっても場面によって(つまりコンテキストによって)必要とするデータは異なり、その結果定義は別々に必要ということになります。ここで、major_category_listとyop_category_listの定義は同一なので、これは一方だけを使うことでも構いません。もちろん、名前だけが違う同一の定義が2つあっても構いません。そこは好みの問題です。定義をよく見ると、parent_category_idがnullのレコードが大分類の項目であり、このリレーションはコンテキストには強く依存せず、どこでも同じ条件で取り出しができることになります。minor_category_listとsub_category_listはどうでしょうか? relationshipの定義が微妙に違います。カテゴリのリストの方は、大分類そのものと関連があるのに対して、メモの方はメモで選択されている大分類項目と関連があります。つまり、これら2つのコンテキストは、別々のものです。つまり、場面が違うものであり別々に定義しなければならないことになります。

ここまで設計すれば、あとは手を動かして実装するだけになります。必要な属性などを追加してページファイルを作り、コンテキスト定義をまとめて定義ファイルを作ります。

ここで、HTMLテンプレートのAllSelectButtonやMinorCategoryButtonを見てください。ここで、ボタンを押して、メモの一覧に検索をかけて絞り込んだり解除したりということが必要になっていますが、INTER-Mediatorの機能ではそれは含まれていません。そういう仕組みのボタンは提供されていないのですなので、ここはどうしてもJavaScriptで実装をしなければなりません。それぞれのボタンは次のように定義しましょう。

<button onclick="redrawMemoList()"
        class="AllSelectButton">全選択</button>
    :
<button onclick="redrawMemoList($)"
        class="MinorCategoryButton"
        data-im="sub_category_list@category_id@$onclick">選択</button>
<div data-im="sub_category_list@category_name"></div>

対応するプログラムは例えば次のように作成します。つまり、memo_listコンテキストに対する検索条件を、状況に応じて切り替えれば良いということです。

function redrawMemoList(id) {
  let idValue = parseInt(id)
  INTERMediator.clearCondition('memo_list')
  if(idValue) {
    INTERMediator.addCondition('memo_list',
        {field:'sub_category_id',operator:'=', value:idValue)
  }
  const context = IMLibContextPool.contextFromName('memo_list')
  INTERMediator.constructMain(context)
}

以上がINTER-Mediatorでの実装設計になりますが、実質的には、ページファイルや定義ファイルがモデルそのものであるとも言えます。一度実装に近いクラス図などを記述するのも良いのですが、結果的にHTMLや定義ファイルを書いてしまった方が早いとも言えます。つまり、デザイン時の作成物と、目的とする作成物が近いというのがこうしたツール(いわゆるローコードツール)の特徴なのではないかと思われます。

そして、INTER-Mediatorでは、エンクロージャー/リピーターの理解は不可欠です。このようなツールごとに理解しておかないといけない特有の概念は必ずあり、それが最も注目すべき制約ということになるのではないかと思われます。

[続開発プロセス#11] MVCパターンベースのWebアプリケーション

これまでの流れで、要求から設計を進めてきました。先にデータベースのスキーマを考える点では、SQLデータベースを使う的なことが早めに決まっているような流れになっていますが、GUIのモックアップから必要な機能を出して設計としてまとめるということをやってきました。ただし、原則としては開発環境の制約に関係なく、要求を元にした必要かつ実現可能性があるような汎用的な設計までができました。ここから、実際に実装が可能な設計を検討します。まず、このような例の場合一番わかりやすい、MVCあるいはMVC2パターンを検討します。

MVCパターンでの制約は、まずは文字通り、モデルとビューを分離して、コントローラーでそれらを統合するという構成にするということがあります。これは制約というより、一種の設計方針として認識されてており、制約という何かできなくなるような「縛り」というよりも、設計を進めやすくする「方針」のような捉え方をされている方も多いと思います。

そして、多くの場合は、HTMLのテンプレートをベースに、クライアントに送り出すHTMLコードを生成します。HTMLがビューであるという見方と、ビューによって生成されたものという見方があり、フレームワークによってその辺りの位置付けは微妙に違っているかと思います。また、テンプレート処理を行うのがサーバー側かクライアント側なのかという点も制約は異なると思われます。ここでは、サーバーサイドで完結するタイプのフレームワークを使っているとしましょう。

主要な制約としてはこのくらいではないかと考えられます。それ以外は、通常は開発言語で記述をするので、自由度は高くなります。巨大なクラスを作っても、小さなクラスに分けても、動くと言えば動きます。

まずは、モデルの抽出です。本質的には、MemoList、PickingUpのクラスで必要な機能が抽出されているので、それを受け付けるという処理を考慮し、テーブルとして用意するmemoとcategoryにそれぞれモデルを割り当てるのが素直な方法と考えられます。CategoryModelの最後の2つのメソッドは、ポップアップメニューの選択肢を構築するためのデータ取得のメソッドになります。

続いてコントローラーとして、MemoListController、CategoryListControllerを導入します。ここで、レイヤー構造となるような大きな区分をパッケージで分類しておきました。UI側の要求をコントローラーで捌き、データベースとのやりとりをモデルによって処理しやすい形式に整えるといった典型的な設計になります。ですが、これは、単にクライアントからの処理が関係しそうな箇所を繋いだだけであって、意図が正しいかを検討しなければなりません。

WebのMVCフレームワークの1つの大きな制約は、クライアントサイドではHTML/JSの世界であり、それがサーバー側でPHPなりJavaなり、異なる世界が展開されていることです。その橋渡しは、テンプレート処理ですが、テンプレート処理はページ構築時に全てを構築することにフォーカスしがちです。

しかしながら、ページを提示後の様々な処理にどう対応するかを、ページのテンプレートの段階で検討しておき、対処をしなければなりません。その後に、HTMLの部分的な要素をテンプレート処理して得ることもあるかもしれませんが、一方、単にJSONで送って返ってくるようなWeb API的な動作が欲しい場合もあるでしょう。

そこで、ここでまず、次の図のようにHTMLのテンプレートを設計します。ページ内の要素に適当に名前をつけてテンプレートそのものをモデルとして記述してみます。事実上、1つのクラスは1つのタグ要素に対応するものに近いレベルで記述しましたが、動作上必要な要素だけが抽出されています。なお、クラス名はタグ名でもいいような気もしますが、ここではそれらしい名前を記述しました。ルートはPageTemplateあり、これがbodyタグと考えても良いでしょう。モックアップの左側にあるカテゴリのボタンが並ぶ部分はCategoryBox、右側でメモの一覧が見えている領域はMemoListとしました。Memoの1行ごとにMemoLineがあり、メモの文字列はMemoTextです。このモデリングは、比較的細かく考えた方が良いでしょう。とは言え、これを書くのは結構面倒なのは確かです。できるエンジニアはこれを頭の中でさっさとやってしまうわけですが、今は開発プロセスの検討をしているので、あえて書いてみました。メモの文字列を表示するinputタグはMemoTextクラスに相当しますが、入力された文字列はvalue属性で得られます。これはブラウザ側の標準機能であるので、ここではprivateの記号で記述しましたが、スコープの意味ではなく、ここでは既定義されているという意味合いでマイナス記号を使いました。

テンプレートの要素をよく見て、その要素が何らかの応答をしなければならない場合には、HTMLelementステレオタイプのクラスはメソッドを記述することにしました。前の図のように、MemoListは書き換えればデータベースへ反映しなければなりません。そういうことで、MemoLineの子要素は全て、update()メソッドを持つ必要があります。update()メソッドのスペックとしては、書き直したら、どこかのタイミングでサーバーに修正データを送り、データベースを更新する必要があるということです。一方、CategoryBox以下のボタンでは、ボタンをクリックすることで、メモ一覧を新たにする必要があり、その意味で、updateMemoList()メソッドの定義があります。さらにこれだけで十分ということではなく、カテゴリが階層化されていて、大分類を変更したら小分類の選択肢が変化するという仕組みが必要になります。つまり、大分類のポップアップメニューを選択すると、小分類のリストをどこかから取ってきて、選択肢を入れ替える必要があります。実装方法はいろいろありますが、この小分類の更新を行うためにMemo_MajourCategoryクラスに、updateMinorCategory()メソッドを定義します。こうして、ページの初期状態をテンプレートとして記述するのはもちろんですが、それがページとして展開された結果を想定して、表示後に必要とする機能をモデルに組み込むことを行います。なお、UI Componentステレオタイプのものは、ここでは詳細設計対象外とします。

テンプレートから元のクラスを検討しましょう。次の図は、テンプレート処理した結果をクライアント側に展開しました。ここで、Webアプリケーションとしての制約があります。クライアントとサーバーの間はHTTP通信によってのみコミュニケーションを取ることができるということは実は大きな制約です。MVC系フレームワークを使う場合、もちろん通信はURLによって柔軟に作り分けることができるのですが、機能を組み込むときに最初に考えることは、それぞれの通信処理でテンプレートを使うかどうかです。ページ全体を生成するときにはテンプレートを利用するのは当然ですが、一方、修正したデータをデータベースに反映する作業はテンプレートを使わなくても良いでしょう。以前だと、サーバーの通信後に常にページ更新をしていたので、それは常にテンプレートを使っていたということになりますが、AJAXを出すまでもなく、現在の仕組みでは単にWeb API的な通信の実装はそれほど難しいことではありません。

ここで、まず、ページを表示するときには、テンプレートをベースに、初期的なメモの表示ルールを適用して、ページ全体を表示します。そのためのメソッドを、MemoListControllerにgetMemoListPage()メソッドとして定義するとします。テンプレートを利用したHTML生成を行って返すことを示すためにTemplatingというステレオタイプをつけておきました。そして、このメモリストでは左側の小分類を押して表示するボタンや、全部のメモを表示するボタンは、同じようにgetMemoListPage()メソッドを使ってページを書き直すのが効率的な設計ではないかと考えられます。この「ページを全部書き直す」作業は、基本的にはHTML上のリンクであり、そのリンクがサーバー側のメソッドを呼び出すので、コントローラーに直接つないでしまっています。ページネーションはコンポーネントとして利用すことにしていましたが、このページネーションも、getMemoListPage()メソッドを使ってページ全体を書き換えるのが1つの方法です(できればリスト部分だけを更新したいですが)。ここの実装はこの設計では細かくは追いませんが、方針としてはMemoListControllerにつながることで進めることにします。

一方、メモの文字列や日付時刻、そして分類の選択肢は、変更をすると、その結果をデータベースに書き直したいわけです。ただし、その作業は、自動的には行えず、要素上で発生したイベントに応じてクライアント側のプログラムを呼び出し、そしてサーバーに要求を伝える必要があります。そのために、クライアントサイドにEditingSupportというクラスを用意しました。もちろん、inputタグ要素の種類に応じて適切なイベントによってこのクラスのメソッドが呼び出されるように、実現可能性を加味した設計にしなければなりません。そして、EditingSupportでは、サーバー側のMemoListControllerにあるupdateMemo()メソッドを適切なパラメータで呼び出すように作ります。こうして、要素の変更からそれがデータベースまで更新される流れがクラス図上で明確に現れてきました。大分類のポップアップメニューを選択した場合、小分類の選択肢を更新するという作業には、ここではPopupUpdateSupportクラスを用意しました。

大分類を示すMemo_MajourCategoryからコールされるのですが、一方でMemo_MinorCategoryのポップアップメニューの選択肢をコントロールできないといけません。select側あるいはoption側のどちらでもいいので適切な参照が必要になり、あらかじめ参照を配列等に記録するか、呼び出し時に更新する要素を指定するなどの方策が必要になります。詳細はここでは省きますが、いずれにしても、JavaScriptで実装すべき内容が、独立したクラスで明確になりました。クラス図なのでクラスで書きましたが、実装じは単に関数でも大きな違いはないと思われます。

ここまで設計を進めれば、MVCフレームワークだと概ねどのフレームワークでも、似たような作業で実装を進めることができるでしょう。フレームワーク特有の事情がある場合は、もちろん、それも考慮して設計を進めます。通常はオブジェクト指向プログラミング環境ですので、クラス図で作った設計との親和性が高いのは当然のことと思われます。

現実の設計では、ここでのコントローラのように、テンプレート処理をしたりしなかったり、あるいは更新処理を受付たり、部分的なHTMLを生成したりと、メソッドによって様々な動作を設計することになります。ここではコントローラーにまとめて書きましたが、むしろメソッドごとに1つのエンティティとして記述した方がわかりやすいかもしれません。フレームワークを利用する場合には、テンプレート処理でスマートにページ生成ができることが強調されますが、ページ表示後の動作についてはそれ以上にたくさんの解決すべき問題を生み出します。フレームワークを使うことで簡素化される面はいろいろあるかとは思いますが、テンプレート処理後の動作を詳細に検討するという作業を、試行錯誤でやっている人は多いのではないかと思います。このように、モデルベースで考えてみれば、どんな仕組みをクライアントとサーバーに持たせるかは全体を見ながら検討できるわけで、より良い側面は多々あります。特に、うまく設計できないで悩んでいるなら、まずはモデルとして設計することは強くお勧めできます。

次回は、同じ設計を別の環境に適用することを考えてみます。