レベル: 初級 Scott Davis, Founder, ThirstyHead.com
2009年 7月 21日 連載「Grails をマスターする」の今回の記事では、Scott Davis が Grails プラグインの世界を紹介します。新しい機能をまるごとアプリケーションに追加するのは、これ以上なく簡単なことです。この記事ではプラグインのマジックの種を明かすとともに、実際にプラグインを使って、Blogito アプリケーションに強力な検索機能を実装してみます。
 |
この連載について
Grails は、Spring や Hibernate などのよく知られた Java 技術に「Convention over Configuration (設定より規約)」といった現代のプラクティスを盛り込んだ最新の Web 開発フレームワークです。Groovy で作成された Grails は既存の Java コードをシームレスに統合するだけでなく、スクリプト言語ならではの柔軟性と動的機能を与えてくれます。Grails を学んだら、Web 開発に対する今までの見方がまったく違ってくるはずです。
|
|
「Grails をマスターする」では連載が始まって以来、コアとなる Grails の機能に焦点に当ててきました。基本要素がどのように構成されているのかを理解すればするほど、基本要素を組み合わせて高度に完成されたアプリケーションを構築するのが簡単になるからです。そのため、プラグインについてはところどころで触れてはいたものの、その詳細を深く掘り下げることは意図的に避けていました。しかし、それも今回までの話です。
今後の何回かの記事では、Grails プラグイン・エコシステムについて読者の皆さんと一緒に探っていきたいと思います。Grails プラットフォームは初めからプラガビリティーを念頭に構築されています。この小さいながらも重要な点に重きを置くことによって、Grails プラットフォームでは文字通り、数百ものプリバンドルされた機能を簡単に利用できるようになっています。
この記事を執筆している時点では、リスト 1 の Groovy スクリプトを実行すると 255 のプラグインが返ってきます (このスクリプトが機能する仕組みについての詳細は、「実用的な Groovy: XML を作成し、構文解析し、容易に扱う」を参照してください)。
リスト 1. 入手可能な Grails プラグインをカウントする単純な Groovy スクリプト
def addr = "http://plugins.grails.org/.plugin-meta/plugins-list.xml"
def plugins = new XmlSlurper().parse(addr)
def count = 0
plugins.plugin.each{
println it.@name
count++
}
println "Total number of plugins: ${count}"
|
これよりも人間が理解しやすいリストを入手するには、コマンド・プロンプトで grails list-plugins と入力するか、Grails Plugins サイト (「参考文献」を参照) にアクセスしてください。
プラグインとは何か
年季の入った Java™ 開発者たちは熟練したハンター兼収集家です。彼らはロギング・ライブラリーを自分で作成しようなどとは夢にも思いません。彼らはただ単に、log4j JAR をクラスパスに指定します。XML パーサーが必要であれば、Xerces JAR をプロジェクトに追加するだけの話です。これらのプラガブルな機能は、オブジェクト指向プログラミングが持つ、再利用可能であるという特徴を表しています。
Grails プラグインもこれと同じ役目を果たしますが、その規模は上回っています。Grails プラグインには数多くの JAR や GSP (GroovyServer Page)、コントローラー、TagLib、サービスなどを組み込むことができます。SiteMesh が 2 つの GSP を 1 つにマージするように、Grails プラグインは、2 つ以上の Grails アプリケーションを効果的に 1 つにマージします。そのため、コアとなるビジネス要件に重点を置きながら、必要な追加機能 (検索、認証、プレゼンテーション層の代替となる機能など) を外部リソースから組み込むことができます。
しかもプラグインは、まさに言葉通り、外部リソースです。Grails 開発チームもいくつかの貴重なプラグインを作成しましたが、プラグインの大部分はコミュニティーから提供されています。事実、Grails チームは毎回 Grails をリリースするたびに、適切であればコア機能をプラグインに分離独立させ、Grails 自体をさらに小型化して安定化を図っています。
それではこの連載で「ごく簡易的な」ブログ・アプリケーションとして作成している Blogito には、プラグインがどのように関係してくるのでしょうか。例えばこのアプリケーションに次に追加したい機能は、ローカル検索機能だとします。検索インフラストラクチャーを自分で一から構築するのではなく、既存のソリューションを取り込みたいのであれば、この先を読んでください。
Searchable プラグインのインストール
Searchable プラグインは、アプリケーションに Google のような検索機能をもたらします。このプラグインは Apache Lucene を利用して索引を作成し、Compass を利用して索引付け機能を GORM (Grails Object Relational Mapping)/Hibernate のライフサイクルにもたらします (「参考文献」を参照)。つまり、ドメイン・クラスを作成、更新、または削除するたびに、それに応じて Lucene 索引が更新されるというわけです。
このプラグインをインストールするには、grails install-plugin searchable と入力します (プラグインをインストールすると何が起こるかについては、次のセクションで技術的詳細を掘り下げます)。
次に、grails-app/domain/Entry.groovy に static searchable = true という 1 行を追加します (リスト 2 を参照)。
リスト 2. Entry クラスを検索可能にする
class Entry {
static searchable = true
static constraints = {
title()
summary(maxSize:1000)
filename(blank:true, nullable:true)
dateCreated()
lastUpdated()
}
static mapping = {
sort "lastUpdated":"desc"
}
static belongsTo = [author:User]
String title
String summary
String filename
Date dateCreated
Date lastUpdated
}
|
ドメイン・クラスは明示的に検索可能にしなければならないことに注意してください。これが何を意味するかと言うと、User クラスに保管されたログインやパスワードなどのインフラストラクチャー・データを引き続き世間の目から隠しておけるということです (索引に含めるクラスとフィールドを指定する方法については、Searchable のオンライン・マニュアルに記載されています。「参考文献」を参照してください)。
上記の 1 行を追加しただけで、Lucene と Compass の機能のすべてが Blogito に追加されました。grails run-app と入力してアプリケーションを起動し、http://localhost:9090/blogito/searchable にアクセスしてください。例えば検索語として grails と入力すると、図 1 の結果が表示されます。
図 1. デフォルトの検索結果
検索結果がいくつか見つかったのは確かですが、記述的な検索結果とは言い難いものです。この点を改善するため、toString() メソッドを Entry.groovy に追加します (リスト 3 を参照)。
リスト 3. toString() を Entry に追加する
class Entry {
static searchable = true
//snip
String toString(){
"${title} (${lastUpdated})"
}
}
|
もう一度、grails を検索してください。図 2 のように、多少なりともユーザー・フレンドリーな検索結果になっているはずです。
図 2. toString() メソッドによる検索結果
これで Searchable プラグイン本来の機能を用意できることがわかったので、次のステップに進めます。次のステップでは、プラグインをアプリケーションの奥深くに統合することです。
プラグイン・インフラストラクチャーの詳細
Blogito ディレクトリーを見渡しても、新しく作成されたファイルは見当たりません。Web ブラウザーで http://localhost:9090/blogito/searchable にアクセスできるとしたら、そこには grails-app/controllers/SearchableController.groovy というファイルがあるはずですが、奇妙なことに該当するファイルはありません。また、lib ディレクトリーには Lucene と Compass の JAR がありそうなものですが、最初に grails create-app と入力してプロジェクトを開始したときと同じく、このディレクトリーは空のままです。実のところ、Blogito で変更されているところは唯一、application.properties に追加された新しい 1 行だけです (リスト 4 を参照)。
リスト 4. 新しくインストールされた Searchable プラグインを示す application.properties
#utf-8
#Wed Jun 24 15:41:16 MDT 2009
app.version=0.4
app.servlet.version=2.4
app.grails.version=1.1.1
plugins.searchable=0.5.5
plugins.hibernate=1.1.1
app.name=blogito
|
plug-ins.searchable という行があることから、Blogito が Searchable プラグインについて認識していることはわかります。それでは、その機能のすべてはどこに隠されているのでしょうか。それを見つけるには、最初にプラグインをインストールしたときに表示された画面出力に戻ればよいだけです。これから、その詳細について説明します。
grails install-plugin searchable と入力すると、まず、プラグインの最新リストを取得するためのリクエストが Web で送信されます。リスト 5 に詳細を示します。
リスト 5. プラグインのマスター・リストのダウンロード
$ grails install-plugin searchable
//snip
Reading remote plugin list ...
[get] Getting: http://svn.codehaus.org/grails/trunk/grails-plugins/
.plugin-meta/plugins-list.xml
[get] To: /Users/sdavis/.grails/1.1.1/plugins-list-core.xml
[get] last modified = Mon Jun 22 04:16:31 MDT 2009
Reading remote plugin list ...
[get] Getting: http://plugins.grails.org/.plugin-meta/plugins-list.xml
[get] To: /Users/sdavis/.grails/1.1.1/plugins-list-default.xml
[get] last modified = Wed Jun 24 06:51:24 MDT 2009
|
コア (plugins-list-core.xml) とデフォルト (plugins-list-default.xml) の 2 つのリストは、作成者、説明、バージョン番号などのプラグインに関するメタデータを提供します。とりわけ重要な点は、Grails はこのメタデータから、プラグインが実際に含まれる ZIP ファイルの URL を検出することです。リスト 6 に、plugins-list-core.xml ファイルに含まれる Hibernate プラグインに関する情報を記載します。
リスト 6. Hibernate プラグインに関する記述
<plugins revision="9011">
<plugin latest-release="1.1.1" name="hibernate">
<release tag="RELEASE_1_1" type="svn" version="1.1">
<title>Hibernate for Grails</title>
<author>Graeme Rocher</author>
<authorEmail/>
<description>A plugin that provides integration between
Grails and Hibernate through GORM</description>
<documentation>http://grails.org/doc/$version</documentation>
<file>http://svn.codehaus.org/grails/trunk/grails-plugins/
grails-hibernate/tags/RELEASE_1_1/grails-hibernate-1.1.zip
</file>
</release>
<!-- snip -->
</plugin>
</plugins>
|
現在、コア・プラグイン・ファイルには Hibernate プラグインしか記載されていません。このリストにあるのは必須プラグイン、つまり Grails が動作するには欠かせない機能です。一方、デフォルト・リストにはコミュニティーから提供されているオプション・プラグインが含まれます。
リスト 5 を見て、これらのファイルがどこに保管されているかがわかりましたか?ホーム・ディレクトリー (UNIX® ライクなシステムでは /Users/<ユーザー名>、Windows® では C:\Documents and Settings\<ユーザー名>) には、.grails というディレクトリーが作成されています。このディレクトリーは、grails run-app と入力して実行した時に、コンパイル済みクラスが保管される場所です。grails clean と入力すると、プロジェクトの下にあるこのアプリケーション・ディレクトリーが削除されます。しかし見てのとおり、.grails はダウンロードされたプラグインが保管される場所でもあります。テキスト・エディターで .grails/1.1.1/plugins-list-default.xml を開いて、Searchable プラグインのエントリーを探してください。リスト 7 にSearchable プラグインに関する記述の詳細を記載します。
リスト 7. Searchable プラグインに関する記述
<plugin latest-release="0.5.5" name="searchable">
<release tag="RELEASE_0_5_5" type="svn" version="0.5.5">
<title>Adds rich search functionality to Grails domain models.
This version is recommended for JDK 1.5+</title>
<author>Maurice Nicholson</author>
<authorEmail>maurice@freeshell.org</authorEmail>
<description>Adds rich search functionality to Grails domain models.
Built on Compass (http://www.compass-project.org/) and Lucene
(http://lucene.apache.org/)
This version is recommended for JDK 1.5+
</description>
<documentation>http://grails.org/Searchable+Plugin</documentation>
<file>http://plugins.grails.org/grails-searchable/
tags/RELEASE_0_5_5/grails-searchable-0.5.5.zip</file>
</release>
<!-- snip -->
</plugin>
|
Grails がプラグインをどこからダウンロードできるかを把握すると、(当然のことながら) 要求されたプラグインを .grails/1.1.1/plugins にダウンロードします (リスト 8 を参照)。
リスト 8. プラグインのダウンロード
$ grails install-plugin searchable
//download core and default plugin lists
// continued...
[get] Getting: http://plugins.grails.org/grails-searchable/
tags/RELEASE_0_5_5/grails-searchable-0.5.5.zip
[get] To: /Users/sdavis/.grails/1.1.1/plugins/grails-searchable-0.5.5.zip
[get] last modified = Thu Jun 18 22:24:45 MDT 2009
|
そして最後に、Grails はダウンロードしたプラグインをローカル・キャッシュからプロジェクトにコピーして解凍します (リスト 9 を参照)。
リスト 9. プロジェクトへのプラグインの追加
$ grails install-plugin searchable
//download core and default plugin lists
//download requested plugin
// continued...
[copy] Copying 1 file to /Users/sdavis/.grails/1.1.1/projects/blogito/plugins
Installing plug-in searchable-0.5.5
[mkdir] Created dir:
/Users/sdavis/.grails/1.1.1/projects/blogito/plugins/searchable-0.5.5
[unzip] Expanding:
/Users/sdavis/.grails/1.1.1/plugins/grails-searchable-0.5.5.zip into
/Users/sdavis/.grails/1.1.1/projects/blogito/plugins/searchable-0.5.5
|
 |
さらに詳細を調べるには
プラグイン・インフラストラクチャーについての詳細が Grails 1.1 のリリース・ノートに記載されています (「参考文献」を参照)。詳細を読んで、プラグインを (新しく作成するすべてのプロジェクトに、その特定のプラグインが自動的に組み込まれるようにするために) グローバルにインストールする方法、代替プラグイン・リポジトリーをリストに追加する方法、プラグインが特定の環境でのみ、または特定の Grails コマンドライン・スクリプトに対してだけ実行されるように制限する方法などを学んでください。
|
|
さらに詳しい内容に進む前に、以下の点を確実に理解しておいてください。プラグインを示す application.properties ファイル内の行は、.grails のプロジェクト・ディレクトリーに解凍されたディレクトリーに対応します。つまり、プラグインをアンインストールするには、grails uninstall-plugin <プラグイン名> と入力することも、あるいは application.properties から該当する行を削除してから、手動で .grails 内のプロジェクト・ディレクトリーから該当するディレクトリーを削除するという単純な方法を使うこともできます。
プラグインが単純な ZIP ファイルとして受け渡しされると認識しておくことも重要な点です。次回の記事では、独自のプラグインを作成し、それをローカル ZIP ファイルからインストールする方法を紹介します (grails install-plugin <プラグイン名> /local/path/to/<プラグイン名>.zip)。プラグインは、リモート URL からインストールすることもできます (grails install-plugin <プラグイン名> http://<ホスト名>/myplugin or<プラグイン名>.zip)。
Searchable プラグインの詳細
Searchable プラグインがインストールされている場所 (.grails/1.1.1/projects/blogito/plugins/searchable-0.5.5) がわかったところで、このプラグインの中身を調べてみましょう。ディレクトリー構造 (図 3 を参照) はお馴染みのように、プラグインとアプリケーションが同じ基本レイアウトを共有する形になっています。
図 3. レイアウトのディレクトリー構造
SearchableController が置かれている場所は、まさにご期待どおり、grails-app/controllers です。このファイルをテキスト・エディターで開いてください。リスト 10 に、このソース・コードの一部を記載します。
リスト 10. SearchableController
import org.compass.core.engine.SearchEngineQueryParseException
class SearchableController {
def searchableService
def index = {
if (!params.q?.trim()) {
return [:]
}
try {
return [searchResult: searchableService.search(params.q, params)]
} catch (SearchEngineQueryParseException ex) {
return [parseException: true]
}
}
//snip
}
|
ご覧のとおり、SearchableService はクラス宣言のすぐ後で、コントローラーに注入されています。デフォルト・ターゲットは、お馴染みの index アクションです。q パラメーターが渡されなければ、空のハッシュマップが grails-app/views/searchable/index.gsp に返されます。この場合、ビューのロジックに従って空のページが表示されます。
index.gsp の 100 行目あたりを見ると、q パラメーターを設定して再帰的に index アクションに自動送信するフォームがあることがわかります。リスト 11 に、このフォームを記載します。
リスト 11. index.gsp 内にある Searchable プラグインのフォーム
<g:form url='[controller: "searchable", action: "index"]'
id="searchableForm"
name="searchableForm"
method="get">
<g:textField name="q" value="${params.q}" size="50"/>
<input type="submit" value="Search" />
</g:form>
|
リスト 10 をもう一度見てみると、q パラメーターに検索語が入力されている場合には、searchableService.search() 呼び出しによる結果が index.gsp に返されることがわかります。返された結果を表示するのは、index.gsp の 150 行目あたりです (リスト 12 を参照)。
リスト 12. 検索結果の表示
<g:if test="${haveResults}">
<div class="results">
<g:each var="result" in="${searchResult.results}" status="index">
<div class="result">
<g:set var="className" value="${ClassUtils.getShortName(result.getClass())}" />
<g:set var="link"
value="${createLink(controller: className[0].toLowerCase() +
className[1..-1],
action: 'show',
id: result.id)}" />
<div class="name"><a href="${link}">${className} #${result.id}</a></div>
<g:set var="desc" value="${result.toString()}" />
<g:if test="${desc.size() > 120}">
<g:set var="desc" value="${desc[0..120] + '...'}" />
</g:if>
<div class="desc">${desc.encodeAsHTML()}</div>
<div class="displayLink">${link}</div>
</div>
</g:each>
</div>
<!-- snip -->
</g:if>
|
Searchable プラグインについては、さらに詳しく調べることを是非お勧めます。具体的には、grails-app/services/SearchableService.groovy を調べてください。lib ディレクトリーでは、ここに組み込まれている Lucene JAR と Compass JAR に注目してください。src/java および src/groovy ディレクトリーをひと通り見て、サポートしているクラスをすべて調べてください。tests ディレクトリーでは、GroovyTestCase を調べてください。典型的な Grails アプリケーションを構成するすべての部分が、このプラグインに含まれています。
新しいプラグインをインストールするたびに、その実装に目を通してください。そうすることが、新しく追加されたすべてのパーツを識別し、これらのパーツがどのように連動するのかを理解する上で役立ちます。そして最も重要な点として、パーツが連動する仕組みを理解することによって、これらのパーツをより密接にアプリケーションに統合するためのヒントを得ることができます。次のセクションでは、デフォルト実装の検索機能を独自のカスタム・コンポーネントに移す方法を説明します。
検索機能と Blogito との密接な統合
ここからは、Entries に独自の検索機能を追加する方法を説明します。まず始めに、テキスト・エディターで grails-app/controllers/EntryController.groovy を開き、リスト 13 に示す search と名付けた単純なアクションを追加します (認証済みでないユーザーがブログ・エントリーを検索できるように、search アクションは忘れずに beforeInterceptor に追加してください)。
リスト 13. search アクションを追加する
class EntryController {
def beforeInterceptor =
[action:this.&auth, except:["index", "list", "show", "atom", "search"]]
def search = {
render Entry.search(params.q, params)
}
//snip
}
|
前のセクションで説明した SearchableService は、すべてのドメイン・クラスでサイト規模の検索を行うには申し分ありません。その一方、Searchable プラグインは個別のドメイン・クラスでも多少のメタプログラミングを行います。Grails が動的に list()、get()、findBy() メソッドを追加するのと同じく、Searchable プラグインは search() メソッドを追加します。
新しい search アクションをテストするため、Web ブラウザーに http://localhost:9090/blogito/entry/search?q=groovy と入力します。図 4 と同じような結果のハッシュマップが表示されることを確認してください。
図 4. そのままの検索結果の表示
search() メソッドが機能することはわかったので、次のステップではユーザー・インターフェースをもう少しユーザー・フレンドリーなものにします。grails-app/views/layouts に _search.gsp という名前の部分テンプレートを作成して、リスト 14 のコードを追加します。
リスト 14. 検索部分テンプレート
<div id="search">
<g:form url='[controller: "entry", action: "search"]'
id="searchableForm"
name="searchableForm"
method="get">
<g:textField name="q" value="${params.q}" />
<input type="submit" value="Search" />
</g:form>
</div>
|
コントローラーは entry に、アクションは search に設定されていることに注意してください。
次は部分テンプレートを表示する番です。grails-app/views/layouts/_header.gsp をテキスト・エディターで開き、render タグを追加します (リスト 15 を参照)。
リスト 15. 検索テンプレートをヘッダーに追加する
<g:render template="/layouts/search" />
<div id="header">
<p><g:link class="header-main" controller="entry">Blogito</g:link></p>
<p class="header-sub">
<g:link controller="entry" action="atom">
<img src="${createLinkTo(dir:'images',file:'feed-icon-28x28.png')}"
alt="Subscribe" title="Subscribe"/>
</g:link>
A tiny little blog
</p>
<div id="loginHeader">
<g:loginControl />
</div>
</div>
|
search <div> が確実に画面の右上端に表示されるようにするため、web-app/css/main.css にちょっとした CSS (Cascading Style Sheets) を追加します (リスト 16 を参照)。
リスト 16. 検索フォームの位置を調整するための CSS
#search {
float: right;
margin: 2em 1em;
}
|
ビューの変更がすべて整ったところで、ブラウザーの表示を更新してください。図 5 のような画面が表示されるはずです。
図 5. ヘッダーに追加された検索フォーム
最後の仕上げとして、現在配置されている単純なデバッグ出力ではなく、HTML で search アクションの実行結果をレンダリングします。そのためには、EntryController の search アクションをリスト 17 のように調整します。
リスト 17. search アクションをより堅牢にする
def search = {
//render Entry.search(params.q, params)
def searchResults = Entry.search(params.q, params)
flash.message = "${searchResults.total} results found for search: ${params.q}"
flash.q = params.q
return [searchResults:searchResults.results, resultCount:searchResults.total]
}
|
このアクションには search という名前が付けられているため、grails-app/views/entry にはこれに対応する search.gsp ファイルを作成する必要があります (リスト 18 を参照)。
リスト 18. search.gsp
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main" />
<title>Blogito</title>
</head>
<body>
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
<div class="body">
<div class="list">
<g:each in="${searchResults}" status="i" var="entry">
<div class="entry">
<h2>
<g:link action="show"
id="${entry.id}">${entry.title}</g:link>
</h2>
<p>${entry.summary}</p>
</div>
</g:each>
</div>
</div>
<div class="paginateButtons">
<g:paginate total="${resultCount}" params="${flash}"/>
</div>
</body>
</html>
|
最後にもう一度、Web ブラウザーで grails を検索してください。図 6 のような結果が表示されます。
図 6. HTML でのわかりやすい検索結果
まとめ
Grails エコシステムのなかで、プラグインは刺激的で活気に満ちた部分です。プラグインによって、事前に用意された多種多様な機能を組み込むことができます。コード・ベースでタッチポイントはどこにあるのかをひとたび理解すれば (application.properties と .grails ディレクトリー)、ソース・コードを探って、プラグイン作成者がそのプラグインのマジックを実装した方法について理解を深められるとともに、独自のコードと密接に統合する方法を思い付くはずです。
次回は、独自のプラグインを作成する方法を紹介します。それまでは、Grails を楽しみながらマスターしてください。
参考文献 学ぶために
製品や技術を入手するために
- Grails: Grails の最新リリースをダウンロードしてください。
- Blogito: Blogito アプリケーションの完成版をダウンロードしてください。
議論するために
著者について
記事の評価
|