目次


Grails をマスターする

カスタム・プラグインを作成する

Grails アプリケーション間で機能を共有する

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Grails をマスターする

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Grails をマスターする

このシリーズの続きに乞うご期待。

連載「Grails をマスターする」ではこれまで多数の記事にわたってコードを賢く再利用する方法に焦点を当ててきました。例えば GSP (GoovyServer Pages) コードの同じスニペットを何ヶ所かでコピー・アンド・ペーストしていることに気付いたら、部分テンプレートやカスタム TagLib を作成することができます。複数のコントローラーやドメイン・クラスで共通するメソッドが 1 つや 2 つあるとしたら、ExpandoMetaClass を使って、メソッドを直接継承または関連付ける抽象親クラスを作成することができます。また、アプリケーションの機能を共有しているとしたら、その機能をサービスやカスタム・コードにリファクタリングすることができます。

しかしこれらの手法はいずれもミクロ・レベルのものです。機能をマクロ・レベルで共有しているとしたらどうなるでしょうか。マクロ・レベルではコントローラーとドメイン・クラス、サービスとコーデック、そして一般的な Grails アプリケーションを構成するその他すべての部分を組み合わせて連動させなければなりません。つまりマクロ・レベルと言っているのは、プラグインのことです。

Grails をマスターする: プラグインを理解する」では、既存のプラグインの 1 つとして Searchable について説明しました。Grails Plugins Portal には、現在 250 を超える Grails プラグインが用意されています (「参考文献」を参照)。しかもこの数は増え続けています。それは、プラグインによって既存の Grails アプリケーションを拡張することが、Grails のコアとなる概念だからです。今回の記事で、独自のカスタム・プラグインを作成する方法を学びます。サンプル・プラグインのソース・コードはダウンロードすることができます。

ShortenUrl プラグインの概要

Twitter.com や携帯電話を利用したテキスト・メッセージのやりとりが盛んなこの時代、メッセージに設けられた文字制限 (例えば Twitter の 140 文字) に収まらない長々とした多くの URL は面倒な問題になります。ありがたいことに、カスタム・プラグインとして事実上 Grails に統合できる URL 短縮用の Web サービスが巷に出回っています。

カスタム・プラグインを作成するには、Grails でのいつもの方法を多少変える必要があります。通常は grails create-app と入力するところを、リスト 1 に記載するように grails create-plugin と入力しなければなりません (このコマンドは既存の Grails ディレクトリーで入力するのではなく、必ず新しい空のディレクトリーで入力するようにします。この新規プラグインを既存の Grails アプリケーションに統合する方法については、記事の最後で説明します)。

リスト 1. カスタム・プラグインを作成する
$ grails create-plugin shortenurl

上記の結果作成されるディレクトリー構造は、通常の Grails アプリケーションとまったく同じです。ただしルート・ディレクトリーには、このプロジェクトがプラグインであることを識別する新しいファイルが 1 つあります。それが、ShortenurlGrailsPlugin.groovy です。リスト 2 にこのファイルのスニペットを記載します。

リスト 2. プラグインの構成ファイル
class ShortenurlGrailsPlugin {
    // the plugin version
    def version = "0.1"
    // the version or versions of Grails the plugin is designed for
    def grailsVersion = "1.1.1 > *"
    // the other plugins this plugin depends on
    def dependsOn = [:]
    // resources that are excluded from plugin packaging
    def pluginExcludes = [
            "grails-app/views/error.gsp"
    ]

    // TODO Fill in these fields
    def author = "Your name"
    def authorEmail = ""
    def title = "Plugin summary/headline"
    def description = '''\\
Brief description of the plugin.
'''

    //snip
}

このファイルには、バージョン番号、プラグインが依存する Grails のバージョン、そしてプラグインが依存する他のプラグインなど、新規に作成するプラグインのメタデータが含まれます (構成ファイルの詳細については、「参考文献」で紹介しているオンライン・マニュアルを参照してください)。

このプラグインを公開プラグインにして、他の開発者が Grails Plugins Portal からダウンロードできるようにしたい場合には、作成者の詳細を入力し、興味をそそるような説明を加えてください。すると、プラグインを Subversion のリポジトリーにチェックインするたびにファイルの内容が読み取られ、Grails Web サイトに自動的に表示されます (プラグインを公開する方法についての詳細は、「参考文献」を参照してください)。この記事では個人で使用するプラグインのままにしておくので、作成者の詳細を入力する部分はそれほど重要ではありません。

ShortenUrl プラグインのために ShortenurlGrailsPlugin.groovy を変更する必要はありませんが、だからといって作業が終わったというわけではありません。ディレクトリー構造の scaffold は生成されているので、次のステップでは実装を作成します。

TinyUrl クラスの作成

TinyUrl.com はよく使われている URL 短縮サービスです。ユーザーが短縮したい URL を TinyUrl.com に送信すると、TinyUrl.com ではその URL が公式の短縮 URL として保存され、その後のすべてのリクエストではその短縮 URL が使用されます。例えば、TinyUrl.com にアクセスして http://www.grails.org/The+Plug-in+Developers+Guide と入力し、Make TinyURL! ボタンをクリックしてみてください。すると図 1 に示されているように、URL は http://tinyurl.com/73495c に短縮されます。これは、元の URL の半分の長さです。

図 1. TinyURL.com によって短縮された URL
TinyURL.com によって短縮された URL
TinyURL.com によって短縮された URL

TinyURL.com がどのように機能するかわかったところで、このサイトの基礎となるサービスを ShortenUrl プラグインに統合する作業に入ります。まずは以下の URL を Web ブラウザーに入力してください。

http://tinyurl.com/api-create.php?url=http://www.grails.org/The+Plug-in+Developers+Guide

このシンプルな Web サービス・インターフェースは、HTML ではなく、指定されたページの短縮された URL だけを返します。

次のステップでは、新たに得た知識を Groovy クラスにカプセル化します。このクラスはまさに真の意味での POGO (Plain Old Groovy Object) であって、つまりサービスでも、コントローラーでも、他の何らかの特殊な目的を持つGrails コンポーネントでもありません。したがって、このクラスを配置するのに最適な場所は src/groovy です。src/groovy の配下に org/grails/shortenurl ディレクトリーを作成し、それから TinyUrl.groovy を作成してリスト 3 のコードを追加します。

リスト 3. TinyUrl ユーティリティー・クラス
package org.grails.shortenurl

class TinyUrl{
  static String shorten(String longUrl){
    def addr = "http://tinyurl.com/api-create.php?url=${longUrl}"
    return addr.toURL().text
  }
}

TinyUrl クラスのテスト

どんなコードでも、本番に移行する前には対応するテストを行うことが必須だと思いませんか?このコードでは直接 Web 呼び出しを行っているため、結合テストをする必要があります。前に作成した org/grails/shortenurl ディレクトリー構造と同じディレクトリー構造を test/integration の下に作成してください。次に TinyUrlTests.groovy を作成し、リスト 4 のコードを追加します (この単純な例では、短いとされる URL は、皮肉なことにエンコードしている元の URL より長くなっています)。

リスト 4. TinyUrl クラスをテストする
package org.grails.shortenurl

class TinyUrlTests extends GroovyTestCase{
  def transactional = false

  void testShorten(){    
    def shortUrl = TinyUrl.shorten("http://grails.org")
    assertEquals "http://tinyurl.com/3xfpkv", shortUrl
  }
}

この結合テストの def transactional = false という行に注意してください。このように設定しないと、リスト 5 に記載する意地の悪いエラー・メッセージが表示されます。

リスト 5. テストで def transactional = false を設定していない場合に発生するエラー
Error running integration tests: java.lang.RuntimeException: 
There is no test TransactionManager defined 
and integration test ${test.name} does not set transactional = false

Grails は各テストをデータベース・トランザクションのなかで完了させようとします。通常の Grails アプリケーションでは問題ありませんが、この場合は完全な Grails アプリケーションではなく、プラグイン内での操作なので、データベースが配置済みであるという前提に立つことはできません。そのため、Hibernate プラグインをインストールするか、あるいはエラー・メッセージの指示に従って結合テストに def transactional = false を設定して対処します。

grails test-app と入力して、テストにパスすることを確認してください。

この後、もう 1 つの URL 短縮サービスを実装しますが、その目的は、このプラグインのユーザーにサービスの選択肢を与えることだけにすぎません。

IsGd クラスの作成

Is.Gd (「イズ・グッド」と発音) サービスの自慢は、TinyUrl.com よりも短いドメイン名、そしてさらに短くエンコードされる URL です。http://is.gd にアクセスして、その Web インターフェースを試してみてください。

これもまた皮肉なことですが、この機会に、TinyUrl.groovy で使用した 2 行のメソッド (リスト 3 を参照) よりも長い実装を以下に記載します。この実装のほうが、サービスの呼び出しに失敗した場合に対処するための情報が多少なりとも多くなります。src/groovy/org/grails/shortenurl 内に IsGd.groovy を作成してください (リスト 6 を参照)。

リスト 6. IsGd ユーティリティー・クラス
package org.grails.shortenurl

class IsGd{
  static String shorten(String longUrl){
    def addr = "http://is.gd/api.php?longurl=${longUrl}"
    def url = addr.toURL()
    def urlConnection = url.openConnection()
    if(urlConnection.responseCode == 200){
      return urlConnection.content.text
    }else{
      return "An error occurred: ${addr}\n" + 
      "${urlConnection.responseCode} : ${urlConnection.responseMessage}"
    }
  }
}

ご覧のように、このコードではレスポンス・コードが 200 (正常であることを意味する HTTP レスポンス・コード) であることを明示的に確認しています (HTTP レスポンス・コードについての詳細は、「参考文献」を参照してください)。簡単のため、呼び出しが失敗した場合にはエラー・メッセージしか返していませんが、この拡張構造を用意しておけば、呼び出しをさらに数回再試行したり、別の URL 短縮サービスに切り替えたりするという方法で、メソッドをさらに堅牢にすることができます。

このコードに対応する IsGdTests.groovy ファイルを test/integration/org/grails/shortenurl ディレクトリーに作成します (リスト 7 を参照)。grails test-app と入力し、IsGd クラスが正常に機能することを確認します。

リスト 7. IsGd クラスをテストする
package org.grails.shortenurl

class IsGdTests extends GroovyTestCase{
  def transactional = false
  
  void testShorten(){
    def shortUrl = IsGd.shorten("http://grails.org")
    assertEquals "http://is.gd/2oCZR", shortUrl        
  }
  
  void testBadUrl(){
    def shortUrl = IsGd.shorten("IAmNotAValidUrl")
    println shortUrl
    assertTrue shortUrl.startsWith("An error occurred:")
  }
}

IAmNotAValidUrl を渡したときに IsGd サービスが失敗する具体的な理由を詳細に確認するには、コマンドラインでリスト 8 に記載する curl を使用することをお勧めします (この cURL ユーティリティーは UNIX®/Linux®/Mac OS X のネイティブ・ユーティリティーであり、Windows® バージョンはダウンロード可能です。「参考文献」を参照してください)。ブラウザーで不適切な URL のテストをすると、エラー・メッセージを確認することはできますが、エラー・コードを確認することはできません。cURL を使用すれば、Web サービスが返すコードが、期待される 200 ではなく 500 であることが明らかにわかります。

リスト 8. curl を使用して失敗した Web サービス・クラスの詳細を確認する
$ curl --verbose "http://is.gd/api.php?longurl=IAmNotAValidUrl"
* About to connect() to is.gd port 80 (#0)
*   Trying 78.31.109.147... connected
* Connected to is.gd (78.31.109.147) port 80 (#0)
> GET /api.php?longurl=IAmNotAValidUrl HTTP/1.1
> User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0) libcurl/7.16.3
                 OpenSSL/0.9.7l zlib/1.2.3
> Host: is.gd
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
< X-Powered-By: PHP/5.2.6
< Content-type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 19 Aug 2009 17:33:04 GMT
< Server: lighttpd/1.4.22
< 
* Connection #0 to host is.gd left intact
* Closing connection #0
Error: The URL entered was not valid.

これで、プラグインのコア機能の実装とテストは完了しました。次は、この 2 つのユーティリティー・クラスを Grails が使いやすいように公開する便利なサービスを作成します。

ShortenUrl サービスの作成

ShortenUrl サービスを作成するには、grails create-service ShortenUrl と入力します。そして grails-app/services/ShortenUrlService.groovy に、リスト 9 のコードを追加します。

リスト 9. ShortenUrl サービス
import org.grails.shortenurl.*

class ShortenUrlService {
    boolean transactional = false

    def tinyurl(String longUrl) {
      return TinyUrl.shorten(longUrl)
    }

    def isgd(String longUrl) {
      def shortUrl = IsGd.shorten(longUrl)
      if(shortUrl.contains("error")){
        log.error(shortUrl)
      }
      return shortUrl
    }
}

前に行った結合テストと同じく、transactional フラグは必ず false に設定してください。これらの呼び出しにはデータベースは一切関与しないため、これらの呼び出しをトランザクションで処理する必要はありません。

isgd() メソッドでは、無効な URL の短縮が試みられた場合は、その試行を必ずログに記録します。すべての Grails 成果物には、実行時に log オブジェクトが注入されます。この log オブジェクトでは、目的のログ・レベル (debuginfoerror など) に対応するメソッドを呼び出すことができます (ロギングについての詳細は、「参考文献」を参照してください)。この後すぐにわかるように、単体テストを作成する際に、この注入された log オブジェクトを処理するには追加のステップが必要です。

Grails がこのサービスを自動作成したときに、サービスに対応するテストも test/unit ディレクトリーに追加されています。このテストは意味的には単体テストではなく結合テストであり、外部リソースに依存してサービスをテストするため、通常であれば、ShortenUrlServiceTests.groovy を test/integration ディレクトリーに移す必要があるところです。しかし今回は単体テストの技をいくつか披露したいので、ShortenUrlServiceTests.groovy ファイルは test/unit ディレクトリーに置いたまま、このファイルにリスト 10 のコードを追加してください。

リスト 10. ShortenUrl サービスをテストする
import grails.test.*

class ShortenUrlServiceTests extends GrailsUnitTestCase {
    def transactional = false
    def shortenUrlService
  
    protected void setUp() {
        super.setUp()
        shortenUrlService = new ShortenUrlService()
    }

    protected void tearDown() {
        super.tearDown()
    }

    void testTinyUrl() {
      def shortUrl = shortenUrlService.tinyurl("http://grails.org")
      assertEquals "http://tinyurl.com/3xfpkv", shortUrl
    }

    void testIsGd() {
      def shortUrl = shortenUrlService.isgd("http://grails.org")
      assertEquals "http://is.gd/2oCZR", shortUrl        
    }

    void testIsGdWithBadUrl() {
      def shortUrl = shortenUrlService.isgd("IAmNotAValidUrl")
      assertTrue shortUrl.startsWith("An error occurred:")
    }
}

transactional フラグを false に設定した後、shortenUrlService 変数を宣言していることに注目してください。続いて setUp() メソッドでサービスの初期化を行います。そして、それぞれのテストごとに setUp() メソッドと tearDown() メソッドを呼び出します。

これが結合テストだとすればエラーなしで実行されるはずですが、これは単体テストなので、testIsGdWithBadUrl() メソッドは No such property: log for class: ShortenUrlService というエラーで失敗します。Web ブラウザーで test/reports/html/index.html を開くと、図 2 に示すエラー・メッセージが表示されます。

図 2. log オブジェクトが注入されていないために失敗した単体テスト
log オブジェクトが注入されていないために失敗した単体テスト
log オブジェクトが注入されていないために失敗した単体テスト

エラーの原因は、単体テストの対象となっているサービスに log オブジェクトが注入されていないことにあります (単体テストは完全に独立して実行されるように意図されていることを思い出してください)。ありがたいことに、このエラーは mockLogging(ShortenUrlService) というたった 1 行を追加することで回避することができます (リスト 11 を参照)。

リスト 11. log オブジェクトが注入されたことにするためのモックの追加
protected void setUp() {
    super.setUp()
    mockLogging(ShortenUrlService)
    shortenUrlService = new ShortenUrlService()
}

mockLogging() メソッドは疑似 log オブジェクトをサービスに注入します。この疑似ロガーは、log4j のアペンダー (appender) に指定された任意の出力先にではなく、System.out に出力を送信します。出力 (図 3 を参照) を表示するには、もう一度 grails test-app と入力し、ShortenUrlServiceTests の HTML レポート・ページの下にある System.out リンクをクリックしてください。

図 3. 疑似ロガーの出力
疑似ロガーの出力
疑似ロガーの出力

このプラグインには、GSP で URL を短縮するカスタム TagLib やカスタム・コーデックなど、他にも多くの Grails 成果物をバンドルすることができますが、プラグインで実現可能な内容は、これで十分把握できたはずです。次は、このプラグインを現状のままパッケージ化し、別の Grails プロジェクトに統合します。

プラグインのパッケージ化とデプロイメント

デプロイメント用の完全な Grails アプリケーションを準備するには grails war と入力しますが、プラグインの場合には grails package-plugin と入力します。すると、プロジェクトのルート・ディレクトリーに grails-shortenurl-0.1.zip ファイルが生成されます。

Grails をマスターする: プラグインを理解する」で説明したように、すべての Grails プラグインは ZIP ファイルとして配布されます。ホーム・ディレクトリーのなかにある grails/1.1.1/plugins ディレクトリーを見てみると、grails-hibernate-1.1.1.zip や grails-searchable-0.5.5.zip など、同じような名前が付いたプラグインがあることがわかります。

ShortenUrl が公開プラグインだったとしたら、grails release-plugin と入力することで、変更を加えたプラグインを Grails Plugins Portal にプッシュすることもできます。そうすれば、grails install-plugin shortenurl と入力することによって、誰でもこのプラグインをプロジェクトに統合することができます。一方、専用プラグインをローカルにインストールするのも同じく簡単で、ローカル・ファイル・システム上の ZIP へのフル・パスを指定するだけで、プラグインをインストールすることができます。

プラグインを実際にインストールしてみるため、shortenurl ディレクトリーの外に新しい空のディレクトリーを作成します。grails create-app foo と入力して単純なアプリケーションを作成し、カレント・ディレクトリーを foo ディレクトリーに変更して grails install-plugin /local/path/to/grails-shortenurl-0.1.zip と入力します。もちろん、パスの部分はプラグインへの実際のパスに置き換えてください。すると、リスト 12 のような出力が表示されます。

リスト 12. ローカル・プラグインをインストールする
$ grails install-plugin /code/grails-shortenurl-0.1.zip
Welcome to Grails 1.1.1 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails

Base Directory: /code/foo
Running script /opt/grails/scripts/InstallPlugin.groovy
Environment set to development
     [copy] Copying 1 file to /Users/sdavis/.grails/1.1.1/plugins
     Installing plug-in shortenurl-0.1
     [mkdir] Created dir: 
     /Users/sdavis/.grails/1.1.1/projects/foo/plugins/shortenurl-0.1
     [unzip] Expanding: 
     /Users/sdavis/.grails/1.1.1/plugins/grails-shortenurl-0.1.zip into 
     /Users/sdavis/.grails/1.1.1/projects/foo/plugins/shortenurl-0.1
Executing shortenurl-0.1 plugin post-install script ...
Plugin shortenurl-0.1 installed

上記を見るとわかるように、ローカルにインストールする専用プラグインのライフサイクルも、公開プラグインのライフサイクルとまったく同じです。

foo/application.properties ファイルをテキスト・エディターで開いて、plugins.shortenurl が記載されていることを確認してください (リスト 13 を参照)。

リスト 13 プラグインが application.properties に記載されていることを確認する
#utf-8
#Wed Aug 19 14:38:24 MDT 2009
app.version=0.1
app.servlet.version=2.4
app.grails.version=1.1.1
plugins.hibernate=1.1.1
plugins.shortenurl=0.1
app.name=foo

プラグインがインストールされていることは確実になったので、今度はこのプラグインが期待通りの動作をすることを確認します。grails create-controller test と入力した後、grails-app/controllers/TestController.groovy を開いてリスト 14 に記載するコードを追加します。

リスト 14. サービスをコントローラーに注入する
class TestController {
    def shortenUrlService

    def index = { 
      render "This is a test for the ShortenUrl plug-in 
" + "Type test/tinyurl?q=http://grails.org to try it out." } def tinyurl = { render shortenUrlService.tinyurl(params.q) } }

このサービスをコントローラーに注入するのは、def shortenUrlService です。grails run-app と入力してアプリケーションを開始し、Web ブラウザーで http://localhost:9090/foo/test/tinyurl?q=http://grails.org にアクセスすると、図 4 の結果が表示されます。

図 4. プラグインが正常にインストールされたことを確認する
プラグインが正常にインストールされたことを確認する
プラグインが正常にインストールされたことを確認する

これで http://tinyurl.com/3xfpkv にアクセスすれば、間違いなく grails.org にアクセスすることができます。

まとめ

以上を読んでおわかりのように、Grails プラグインを作成する方法は通常の Grails アプリケーションを作成する方法とそれほど変わりません。grails create-app と入力する代わりに grails create-plugin と入力し、grails war と入力する代わりに grails package-plugin と入力するといった具合です。そして GrailsPlugin.groovy 記述子ファイルに重要な詳細を追加するという点を除けば、Grails プラグインを作成する場合でも、Grails アプリケーションを作成する場合でも、手順 (サービスの作成、テストの作成など) はすべて同じです。

今回の記事では、mockLogging() メソッドによる Grails 単体テストのモック作成機能について簡単に触れましたが、次回は mockDomain()mockForConstraintsTests() をはじめ、さらに多くの非常に有益な mock メソッドについて説明します。それまでは、Grails を楽しみながらマスターしてください。


ダウンロード可能なリソース


関連トピック

  • Grails: Grails の Web サイトにアクセスしてください。
  • Grails Plugins: Grails プラグイン・ポータルにアクセスして、Grails フレームワークに用意された最新のプラグインに関する情報を入手してください。
  • The Plug-in Developers Guide: Grails プラグイン開発者向けのオンライン・マニュアルです。
  • Creating Plugins: Grails プラグインの作成、配布、インストール方法に関する詳細を調べてください。
  • TinyURL: ウィキペディアで TinyURL に関する記事を読んでください。
  • HTTP ステータス・コード: ウィキペディアに、HTTP ステータス・コードをすべて網羅したリストが記載されています。
  • Logging: Grails でのロギング構成について詳しく学んでください。
  • Grails Framework Reference Documentation: Grails のバイブルです。
  • Groovy Recipes』(Scott Davis 著、Pragmatic Programmers、2008年): Scott Davis の最新の著作で Groovy と Grails の詳細を学んでください。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
  • Grails: Grails の最新リリースをダウンロードしてください。
  • cURL: ほとんどの UNIX、Linux、および Mac OS X システムでは、cURL がデフォルトでインストールされます。Windows やその他ほとんどすべての OS に対応したバージョンもダウンロードできます。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Web development, Open source
ArticleID=438133
ArticleTitle=Grails をマスターする: カスタム・プラグインを作成する
publish-date=09152009