プーシキン全集 Web コンコーダンス・プログラムの Web インタフェース部分は Wt C++ Web Toolkit(以下 Wt)を使って拵えた。この C++ ライブラリは,Widget(画面部品オブジェクト)ベースで Web 画面を構成しつつ,ビジネスロジックも C++ で書いて行くスタイルを想定したものである。今日はコンコーダンス・プログラムで覚えた Wt のサワリ(奥が深いのでサワリに過ぎない)についてメモを残しておく。
最近では,Web のサーブレットを Java で作成し,ビジネスロジック(昔からある基幹システムは多く C/C++ で書かれている)とは Java JNI や CORBA,RPC,SOAP で接続し,画面周りは JSP ないしは HTML + JavaScript ベースで準備するのが一般的のように思われる。こうすることで,Web デザインとビジネスロジックを分割して,前者を Web デザイナ,後者をプログラマが担当するというふうに,それぞれのプロフェショナルに作業分担を行うのがやりやすい,というメリットがある。私の担当システムでもこのやり方が主流である。
この潮流からすると,Wt のデザインパターンは一見,時代に逆行しているように私には思われた。でも実際にこれでプログラムを書いてみると,確かに HTML ほど画面構築は簡単ではないが,ヘタにデザイナに弄らせるとまずい Web フォームのような基本インタフェースを含め,コンテンツ HTML の枠組みマークアップ部分をシステム屋・プログラマに任せつつ,「見た目」のデザイン自体はスタイルシートとしてデザイナに任せ,説明文など文字コンテンツについては外部 XML の編集という形で運用者・(プログラマでない)プロジェクト要員に分担させることができる,と納得した。Wt は,総じて,極めてよく考えられたプラットフォームであることがわかったのである。いまや Web デザインは HTML ではなく CSS でこそ実現する時代なのである。
既存ソフトウェア資産は C/C++ が多いし,Java プログラマよりも C/C++ プログラマのほうが圧倒的に人材が豊富なので,プロジェクトの運営もしやすいと私は思う。Wt なら C++ であらゆる Web コードが実現でき,比較的高速に動作するし,C/C++ の膨大な既存資産との連携に関しても,Java よりもずっと悩みが少ない。しかも,画面 HTML,JavaScript をまったく書かずに Ajax 非同期通信の高速 Web サーバ・アプリケーションを構築できる。また,システムの作り・ビジネスロジックを完全に隠蔽することができる(どんなスタイルシートを使っているのかすらユーザにはわからないし,リンクすら見えずロボットで画像を根こそぎパクられることも回避できる。システムが生成した画面の HTML から見えるのはシステムが動的に生成・送信した,わけのわからない JavaScript ばかりである)。Gdb シンボリック・デバッガを用いて Web アプリをデバッグできるところも私には魅力である。「何でいまさら Web アプリを C++ で?」という方はぜひお試しいただきたいと思う。
Wt ライブラリのインストールについてはブログ記事 Wt C++ Web Toolkit にしるしたので,そちらを参照。ただし,その記事には FastCGI について記述がなかったので,もし Wt で作成した Web サーバプログラムを FastCGI で動作させたいのであれば,Wt インストールに先立って,FastCGI のデベロッパライブラリと Apache モジュールをシステムに組込んでおかなければならない。これらは FastCGI のサイトのダウンロードページから,それぞれ,fcgi-
Wt ベースのコンコーダンス Web アプリは以下のようなものである。ただし,Wt に関係の薄いコンコーダンス・ビジネスロジック部分は省略してある。
/* -*- coding: utf-8; mode: c++; -*-
* Concordance to A. S. Pushkin's Works
* $Id: insomnia.txt 222 2014-03-27 11:23:12Z isao $
* Copyright (C) 2012, isao yasuda
*/
#include <Wt/WApplication>
#include <Wt/WContainerWidget>
#include <Wt/WEnvironment>
#include <Wt/WLineEdit>
#include <Wt/WGroupBox>
#include <Wt/WButtonGroup>
#include <Wt/WRadioButton>
#include <Wt/WPushButton>
#include <Wt/WCheckBox>
#include <Wt/WText>
#include <Wt/WBreak>
#include <turglem/lemmatizer.hpp>
#include <turglem/russian/charset_adapters.hpp>
#include <boost/tokenizer.hpp>
#include <boost/locale.hpp>
#include "ShmCorpusWordTree.hpp"
using namespace Wt;
using namespace boost::interprocess;
typedef std::pair<std::string, std::string> string_type;
/* コンコーダンス用グローバル変数,関数の記述。省略 */
// コンコーダンスクラス
class Concordance : public WApplication
{
WLineEdit* expEdit;
WButtonGroup* wbg0;
WButtonGroup* wbg1;
WGroupBox* gbox;
WCheckBox *g0, *g1, *g2, *g3, *g4, *g5, *g6, *g7,
*g8, *g9, *g10, *g11, *g12; // 00-11 + all
WText* wmesg;
WText* pmesg;
WText* cmesg;
void clearvse();
void clearother();
void generate();
void errorsend(const std::string emsg);
public:
Concordance(const WEnvironment& env);
};
Concordance::Concordance(const WEnvironment& env)
: WApplication(env)
{
// メッセージファイル message.xml の指定
messageResourceBundle().use(appRoot() + "message");
// タイトル
setTitle(L"Динамический Конкорданс к сочинениям А. С. Пушкина");
// スタイルシート
useStyleSheet("style.css");
// message.xml から header を出力
root()->addWidget(new WText(WString::tr("header"), XHTMLText));
root()->addWidget(new WText(WString::tr("subheader"), XHTMLText));
root()->addWidget(new WBreak());
// 入力プロンプト(初期値付き))
root()->addWidget(new WText(L"Выражения ", XHTMLText));
// expEdit = new WLineEdit(L"^П[ЕЁ]СТР.*", root());
// 入力エリア: Enter key で submit
expEdit = new WLineEdit(root());
expEdit->setEmptyText
(L"Вводите Слова или Регулярные выражения, напр.: п[её]стр.*");
expEdit->setTextSize(70);
// expEdit->setFocus(); // focus より説明テキスト
expEdit->enterPressed().connect(this, &Concordance::generate);
// submit ボタン
WPushButton *button = new WPushButton(L"поиск", root());
button->setMargin(10, Left);
// ジャンル一覧へのリンク(message.xml)
root()->addWidget(new WText(WString::tr("link"), XHTMLText));
root()->addWidget(new WBreak());
// 見出語形/出現形選択ラジオボタン
WGroupBox *gbox0 = new WGroupBox(L"Форма Базы данных", root());
gbox0->setId("gbx0");
wbg0 = new WButtonGroup(gbox0);
WRadioButton *rb0;
rb0 = new WRadioButton(L"Lemmatized (форма заглавных слов)", gbox0);
wbg0->addButton(rb0, 0);
rb0 = new WRadioButton(L"Non Lemmatized (форма в тексте)", gbox0);
wbg0->addButton(rb0, 1);
wbg0->setCheckedButton(wbg0->button(0)); // Lemmatized
// 入力を見出語変換(見出語形の場合のみ)
WGroupBox *gbox1 = new WGroupBox
(L"Лемматизация входа", root());
gbox1->setId("gbx1");
wbg1 = new WButtonGroup(gbox1);
WRadioButton *rb1;
rb1 = new WRadioButton(L"ON (только в случае Lemmatized)", gbox1);
wbg1->addButton(rb1, 0);
rb1 = new WRadioButton(L"OFF", gbox1);
wbg1->addButton(rb1, 1);
wbg1->setCheckedButton(wbg1->button(0)); // YES
// ジャンル選択チェックボックス
gbox = new WGroupBox(L"Выбор жанров", root());
gbox->setId("gbx");
g12 = new WCheckBox(L"Все", gbox);
g0 = new WCheckBox(L"Стихи", gbox);
g1 = new WCheckBox(L"Поэмы", gbox);
g2 = new WCheckBox(L"Сказки", gbox);
// その他チェックボックス設定は省略
// ジャンルデフォルト: すべて
g12->setChecked(true);
// チェック時イベント処理登録
g0->checked().connect(this, &Concordance::clearvse);
g1->checked().connect(this, &Concordance::clearvse);
g2->checked().connect(this, &Concordance::clearvse);
// その他チェックボックス設定は省略
g12->checked().connect(this, &Concordance::clearother);
// ラベル表示,ユーザ入力内容表示エリア
wmesg = new WText(root());
pmesg = new WText(root());
root()->addWidget(new WBreak());
// コンコーダンス出力エリア
cmesg = new WText(root());
root()->addWidget(new WBreak());
// フッタを message.xml から出力
root()->addWidget(new WText(WString::tr("footer"), XHTMLText));
root()->addWidget(new WBreak());
// 入力内容で submit
button->clicked().connect(this, &Concordance::generate);
}
// エラーメッセージ出力
void Concordance::errorsend(const std::string emsg)
{
WString s(emsg, UTF8);
s = "<p class=\"err\">" + s + "</p>";
cmesg->setTextFormat(XHTMLUnsafeText);
cmesg->setText(s);
}
// オール以外クリア
void Concordance::clearother()
{
// オール指定がチェックされていたらほかをクリア
if (g12->isChecked()) {
g0->setChecked(false); g1->setChecked(false); g2->setChecked(false);
// その他設定省略
}
}
// オール指定クリア
void Concordance::clearvse()
{
// オール指定以外がチェックされていたらオール指定をクリア
if (g0->isChecked()) g12->setChecked(false);
if (g1->isChecked()) g12->setChecked(false);
// その他設定省略
}
// コンコーダンス生成
void Concordance::generate()
{
const WEnvironment& env = WApplication::instance()->environment();
// ユーザ入力単語式: WT::WString を std::string に toUTF() で変換
std::string ldata = (expEdit->text()).toUTF8(); // 元入力
std::string userip = "IP: " + env.clientAddress() + " Req: " + ldata;
logging(userip.c_str(), 0);
// 大文字変換
std::string udata = boost::locale::to_upper(ldata);
// 実行 Word Tree の選択,Lemmatizer 実行可否判断
offset_ptr<tree> word_tree; // 実行 Word Tree
WString opts(ldata, UTF8);
opts = L" (Ваш вход: " + opts;
// Lem Tree on なら見出語形ツリー選択
if (wbg0->checkedId() == 0) {
word_tree = word_tree_l; // select Lemmatized
opts += L"; DB: Lemmatized; Лемматизация входа: ON)";
// Lemmatizing on なら Lemmatizer で入力を見出語変換する
if (wbg1->checkedId() == 0) {
lemmatizer(udata); // 見出語変換
}
} else {
// off なら出現形ツリー選択
word_tree = word_tree_a; // select Non Lemmatized
opts += L"; DB: Non Lemmatized; Лемматизация входа: OFF)";
}
// ユーザ入力をトークナイザで分解し,式を vector にセット
std::vector<std::string> wds;
exp_tokenizer(udata, wds);
if (wds.empty()) {
errorsend("No expressions specified.");
return;
}
// 対象ジャンルのセット: global int vector にセット
// - g0 から順にチェックを確認し,true なら push_back
// - 全選択がチェックされている場合は vector を空にする
// - ひとつも選択がない場合は全選択となる
genrev.clear();
if (g0->isChecked()) genrev.push_back(0);
if (g1->isChecked()) genrev.push_back(1);
if (g2->isChecked()) genrev.push_back(2);
// 中略
if (g12->isChecked()) genrev.clear();
if (genrev.empty())
grall = true;
else
grall = false;
// ラベルの出力
wmesg->setTextFormat(XHTMLText);
wmesg->setText(L"<div class=\"blk\"></div><br />Выражения: ");
// ユーザ入力内容の表示
// 変換済ユーザ入力
WString wdata(udata, UTF8);
wdata += opts;
pmesg->setTextFormat(PlainText);
pmesg->setStyleClass("exp");
pmesg->setText(wdata);
/* 以下にビジネスロジック cmesg に出力コンコーダンスを格納する。省略 */
// コンコーダンスの出力
cmesg->setTextFormat(XHTMLUnsafeText);
cmesg->setText(conc);
}
// サービスオブジェクトの生成
WApplication *createApplication(const WEnvironment& env)
{
return new Concordance(env);
}
// main: 各種 Web パラメータとともにサービスオブジェクトを生成する
int main(int argc, char** argv)
{
/* Web サービスの初期処理を行う。省略 */
// サービス開始
return WRun(argc, argv, &createApplication);
}
このように,Concordance クラスとそのクラスメソッドとして,画面,ビジネスロジックを記述して行く。WApplication クラス継承として Concordance クラスを生成し,Wt::
プーシキン・コンコーダンス画面では,テキストボックス,送信ボタン,チェックボックス,ラジオボタンをコンコーダンス・ビジネスロジック動作条件の設定のために用いている。これらの画面要素を Concordance クラスのコンストラクタのなかで Wt Widget オブジェクトとして定義している。 画面の「地」は,WApplication クラスのメソッドである root() によってオブジェクトのポインタが返されるようになっていて,これに紐付けて各種 Widget を new 演算子で生成して行く。
上例では,チェックボックスやラジオボタンを WGroupBox,WButtomGroup といった,ボタンやチェックボックスを纏める Widget のなかに入れており,この場合は root() にバインドされたこれらグループに紐付けてボタン等の Widget を生成する。 root() に付随するすべての画面オブジェクトは,プログラムが終了する時点で再帰的に解放されるので,プログラマがポインタを管理し delete を発行する必要はない。提供 Widget,インクルードすべきヘッダ等,その使い方の詳細は Wt ドキュメント,Widget Gallery デモプログラムを参照のこと。
テキストデータは Wt::WString クラスオブジェクトとして扱う必要がある。Wt::
useStyleSheet("スタイルシートファイル名") でスタイルシートの指定ができる。Widget オブジェクトに対し,setClass(),setId() メソッドで class や id の HTML 要素を付加でき,「見た目」のコントールを,指定したスタイルシートで定義することが可能である。
上例の messageResourceBundle().use(appRoot() + "message"); 行は,外部 XML ファイルリソースの指定である。この場合 message.
<?xml version="1.0" encoding="UTF-8" ?>
<!--
message.xml: コンコーダンス用メッセージファイル
-->
<messages>
<message id="header">
<div id="header">
<h1>Конкорданс к тексту А. С. Пушкина</h1>
</div>
</message>
<!-- 後略 -->
</messages>
と記述しておくと,Wt::
Wt ではアプリケーションを動作させる環境として二種類をサポートしている。ひとつは Wthttpd という独自のスタンドアロン Web Socket サーバとして,いまひとつは Apache2 等の Web サーバから起動する FastCGI のアプリケーションとして動作させることができる。この際,ソースコードはまったく同じであり,これらの違いはリンケージするとき,libwthttp と libwtfcgi のどちらを使うかに依存する。プーシキン・コンコーダンスの場合は FastCGI で動作させているので,コンパイル/リンクは以下のとおりである。
% g++ -g -I/usr/local/include -L/usr/local/lib \ -boost_random -lboost_signals -lboost_system -lboost_thread \ -boost_filesystem -lboost_program_opions -lboost_date_time \ -lboost_regex -lboost_locale \ -licudata -licui18n -licuuc \ -lwtfcgi -lwt \ -o concordance concordance.cpp \ -lturglem -lturglem-russian -lMAFSA
-lwtfcgi -lwt のライブラリ・リンケージ指定が FastCGI 運用の条件である。Boost の各種ライブラリも Wt に必要である(すべてというわけではない)。ICU Unicode ライブラリはコンコーダンスのロシア語正規表現で使うもの。最後の三つのライブラリは Lemmatizer である。
FastCGI を Apache から使えるようにするには,Apache の httpd.conf に,モジュールを追加するだけでよい。モジュールのパスはインストールしたところを指すようにする。
LoadModule fastcgi_module libexec/apache22/mod_fastcgi.so
Wt Web アプリを FastCGI として動かすには,以下のような設定を外部ファイル(fastcgi.
<IfModule mod_fastcgi.c>
Alias /pushkin/lemmatized/ /home/isao/src/pushkin/concordance/
FastCgiConfig -idle-timeout 600 -maxClassProcesses 20 -maxProcesses 60 \
-killInterval 1200 -autoUpdate \
-initial-env CONCORDANCE_CONFIG=beatrice.conf \
-initial-env WT_AP_ROOT=/etc/wt
FastCgiServer /home/isao/src/pushkin/concordance/concordance
</IfModule>
上のコンコーダンス・プログラム例でいえば,/home/
FastCgiConfig 行は必要に応じて指定すればよい。-initial-env パラメータで,アプリに渡す環境変数を設定することができる。通常の Apache 環境変数設定手段である SetEnv ディレクティブは FastCGI では使えないので注意する必要がある。その他のパラメータについては,FastCGI ドキュメント Module mod_fastcgi を参照。
Apache をリスタートすればアプリが動くはずである。Apache エラーログ httpd-
FastCGI ではなく Wthttpd で動かすのなら,リンクライブラリを -lwthttp -lwt とすればよい。そして,以下のようにプログラムを起動する。
% concordance (プログラムロードモジュール名) --docroot . \ --http-address 0.0.0.0 --http-port 8080 &
無事起動できれば,http://
Wt では /etc/