はじめに


Grailsは、Java仮想マシン上で動作するフルスタックのWebアプリケーションフレームワークです。Javaベースのスクリプト言語「Groovy」でプログラムを記述します。
このコラムでは、ファイルアップローダの作成をとおして、基本的なGrailsアプリケーション開発の流れを紹介します。また同時に、Grailsアプリケーション開発において知っていると便利ないくつかの実践的なテクニックも紹介します。
Grails 2.1.0を用いてこのコラムの検証をしていますが、Grails 2.0.0以上であれば同じようにアプリケーションを作成できるでしょう。

  

環境構築

環境構築

もしあなたがGrailsに触れたことがないなら、まずJDKとGrails本体のインストールが必要です。このコラムでは細かい環境構築の説明は割愛しますが、JGGUG(日本Grails/Groovyユーザーグループ)有志にて運営されているGrails.jpの和訳ドキュメントにあるスタートガイドを参考に環境を構築すると良いでしょう(簡単ですよ!)。

あるいは、VMware社が公開しているEclipseベースの統合開発環境(IDE)であるGroovy/Grails Tool Suite (GGTS)を使う、という方法もあります。これには、Grailsも同梱されています。

GGTSのダウンロードサイトには、Windows用、Mac OS X用、Linux用のインストーラが用意されています(Mac OS X用、Linux用は「 OTHER DOWNLOADS > 」をクリックすると表示されます)。もし64bit版のJDKを利用している場合には、GGTSも64bit版を選択するようにしてください。

GGTSを日本語化したい場合は、Pleiades の最新版が利用可能ですが、その場合、GGTS.ini

-vmargs
-javaagent:plugins/jp.sourceforge.mergedoc.pleiades/pleiades.jar
-Dosgi.requiredJavaVersion=1.5

といった形で
-javaagent:plugins/jp.sourceforge.mergedoc.pleiades/pleiades.jar
の1行を追加する必要があることに注意してください。

作成するアップローダについて

これから作成するファイルのアップローダは、アップロードするときにダウンロード用のキーフレーズと削除用のキーフレーズを指定し、それぞれ正しいフレーズが入力されたときのみダウンロードや削除が行えるようにするものです。また、キーフレーズは平文ではなくハッシュ化して保存します。

< 作成するファイルアップローダのファイル一覧画面 >

作成するファイルアップローダのファイル一覧画面

< 作成するファイルアップローダのファイルアップロード画面 >

作成するファイルアップローダのファイルアップロード画面

また、このコラムでは、Groovyで広く使われる変数宣言のdefをなるべく使わず、変数の型を明記する形でソースコードを記述しています。Javaに慣れた技術者がこのコラムを読んだとき、より違和感の少ないように、またIDEでの補完を考慮すると型を明記した方が有利であること、などの理由によるものです。defを使うことに対して何らかの制限がある、というわけではありません。

では、順を追って作っていきましょう。


アプリケーションとドメインクラスの作成

アプリケーションの作成

$ grails create-app uploader

として、これから作成するuploaderアプリケーションを作成します。

これ以降の作業は、ここで作成されたuploaderディレクトリの配下で行います。GGTS上では、Grailsプロジェクトを新規作成することで同様にアプリケーションを作成できます。もしWindows上で作業する場合、作成するファイルの文字コードをShift-JIS(MS932)ではなく、UTF-8にするよう注意しましょう。

ドメインクラスの作成 / FileEntry.groovy

$ grails create-domain-class org.jggug.kobo.uploader.FileEntry

というコマンドで、FileEntryというドメインクラスの雛形を作ることができます。GGTSの場合も、「Open Grails Command Prompt」から同様にGrailsコマンドを実行することができます。
生成されたファイルは grails-app/domain/org/jggug/kobo/uploader/FileEntry.groovy に出力されています。ここに必要な記述をしていきましょう。

package org.jggug.kobo.uploader

class FileEntry {
	String fileName    // アップロードされたファイル名
	String fileComment // アップロードファイルに対して付けるコメント(自由記述)
	Integer fileSize   // アップロードされたファイルのサイズ
	Date dateCreated   // 作成日時(アップロードされた日時)
	String downloadKey // ダウンロードする際に必要なパスワード
	String deleteKey   // 削除する際に必要なパスワード
	byte[] fileData    // アップロードされたファイルそのもの

	static mapping = { fileData( type: 'materialized_blob' ) }

	static constraints = {
		fileName( blank: false )
		fileComment()
		fileSize( blank: false )
		dateCreated()
		downloadKey( password: true )
		deleteKey( password: true, blank: false )
		fileData()
	}
}

fileName、fileSize、deleteKeyに対して指定しているconstraintsの blank: false では、空白を許可しないという制約を付加しています。

dateCreated には、FileEntry のインスタンスが作成された日時が Grails によって自動的に格納されます。Grailsのドメインクラスにおいて、dateCreated とlastUpdated の2つのプロパティ名は、それぞれインスタンスの作成日時、更新日時が格納されるという規約があり、これを利用しています。作成日時・更新日時を保存する、という処理は非常に多く求められるものですので、そうしたときにこの dateCreated とlastUpdated を使うというテクニックを思い出してください。

downloadKey と deleteKey には、constraintsでpassword: true を指定しています。これは、ドメインクラスからScaffoldによってビューを生成したときに、パスワード入力のフォームを生成するための指定です。

fileDataには、アップロードするファイルのバイナリデータが格納されます。GrailsのORマッパーであるGORM (Grails Object Relational Mapping)では、DBへのマッピングに関する指定をドメインクラスのmappingで行います。ここでは、fileData に対し、materialized_blob型として扱うよう指定することで、DB上はBLOB型として格納し、Grails上では byte[] 型として扱えるようにしています。

mappingにおける type の指定では、GrailsがORマッピングの実装として利用しているHibernateで定義されているマッピング型のいずれかを指定します。もし、DBで定義されているSQLで直接使うことのできる型を指定したい場合には、sqlTypeを指定することもできます。

ビューとコントローラの生成・編集

Scaffoldによるビューとコントローラの生成

先ほど作成したドメインクラス FileEntry から、Grailsコマンドを用いてビューとコントローラを自動生成します。

$ grails generate-all org.jggug.kobo.uploader.FileEntry

もし、ビューやコントローラを別々に生成したい場合、generate-viewsやgenerate-controllerというコマンドを同様に利用することができます。

$ grails run-app

のコマンドで作成中のGrailsアプリケーションを起動できるので、この段階でどういう動作をするのか試してみると良いでしょう。run-appすると

| Server running. Browse to http://localhost:8080/uploader/

という表示が出てくるので、このURLにブラウザからアクセスすると、こんな画面が表示されます。

< デフォルトのインデックス画面 >

デフォルトのインデックス画面

ここで org.jggug.kobo.uploader.FileEntryController のリンクをクリックしてみてください。Scaffoldで自動生成したビューとコントローラが動作しているのが分かります。

コントローラの編集 / FileEntryController.groovy

ファイルのダウンロードに対応するよう、FileEntryControllerにアクションdownloadを追加します。また、allowedMethodsにdownloadの記述を追加します。これによって、downloadアクションは、POSTメソッドによるHTTPリクエストでのみ実行可能になります。ファイルは
grails-app/controllers/org/jggug/kobo/uploader/FileEntryController.groovy
にあります。

	static allowedMethods = [save: "POST", update: "POST", delete: "POST", download: "POST"]

	def download() {
		String downloadKey = params.downloadKey.encodeAsSHA1()
		
		FileEntry entry = FileEntry.findById(params.id)
		if (entry.fileData && entry.downloadKey == downloadKey) {
			response.contentLength = entry.fileData.size()
			String filename = entry.fileName.encodeAsURL().replace("+", "%20")
			response.setHeader("Content-Disposition","attachment; filename=$filename;")
			response.outputStream.write(entry.fileData)
		} else {
			response.sendError(403)
		}
	}

このアクションは、フォームに入力されたdownloadKeyのSHA-1ハッシュ値が保存されているファイルのdownloadKeyと同一の場合にのみ、ファイルがダウンロードされる動作をします。ファイルがアップロードされたときのファイル名を復元するため、encodeAsURL() で fileName をURLエンコードし、response.setHeader() でContent-Dispositionヘッダーを追加してレスポンスを返します。encodeAsURL() によってファイル名に含まれるスペースが "+" になり、"File+Name.png" のようなファイル名でダウンロードされるため、スペースをパーセントエンコードした "%20" に置換し "File Name.png" のように元のファイル名が復元されるように .replace("+", "%20") をしています。

ファイルをアップロードする際に呼び出されるsaveアクションについても、以下のように修正します。

	def save() {
		CommonsMultipartFile fileData = params.fileData
		FileEntry fileEntryInstance = new FileEntry(params)
		fileEntryInstance.fileName = fileData.originalFilename
		fileEntryInstance.fileSize = fileEntryInstance.fileData.size()
		fileEntryInstance.downloadKey = fileEntryInstance.downloadKey.encodeAsSHA1()
		if(fileEntryInstance.deleteKey){
			fileEntryInstance.deleteKey = fileEntryInstance.deleteKey.encodeAsSHA1()
		}
		if (!fileEntryInstance.save(flush: true)) {
			fileEntryInstance.downloadKey = ""
			fileEntryInstance.deleteKey = ""
			render(view: "create", model: [fileEntryInstance: fileEntryInstance])
			return
		}

		flash.message = message(code: 'default.created.message', args: [fileEntryInstance.fileName, fileEntryInstance.id])
		redirect(action: "list")
	}

ここでは、フォームからアップロードされたファイルからファイルサイズとファイル名を取得し、downloadKeyとdeleteKeyは、SHA-1でエンコードしたハッシュ値として保存するようにしています。

deleteKey については、もし空文字が送信されてきた場合、SHA-1ハッシュ値の計算をしないようにしています。deleteKeyが空文字のまま、fileEntryInstance.save(flush: true) が行われると、ドメインクラスで宣言されているconstraintsのblank: falseに従ってバリデーションが行われ、このバリデーションに失敗するためDBへの保存が行われません。DBへの保存が行われなかった場合には、create.gspのビューを再表示します。このとき、downloadKeyとdeleteKeyはクリアしておくようにします。

CommonsMultipartFileというのは、org.springframework.web.multipart.commons.CommonsMultipartFileです。ファイルの冒頭にimport文を書いておくか、完全修飾名で型を記述しましょう。defで代替しても良いですが、IDEの補完を受けようとする場合には不便です。

ファイルを削除する際に呼び出されるdeleteアクションは、以下のように修正します。

	def delete(Long id) {
		FileEntry fileEntryInstance = FileEntry.get(id)
		if (!fileEntryInstance) {
			flash.message = message(code: 'default.not.found.message', args: [message(code: 'fileEntry.label', default: 'FileEntry'), id])
			redirect(action: "list")
			return
		}
		
		String deleteKey = params.deleteKey.encodeAsSHA1()
		if (fileEntryInstance.deleteKey != deleteKey) {
			flash.message = message(code: 'default.not.deleted.message', args: [fileEntryInstance.fileName, id])
			redirect(action: "list")
			return
		}

		try {
			fileEntryInstance.delete(flush: true)
			flash.message = message(code: 'default.deleted.message', args: [fileEntryInstance.fileName, id])
			redirect(action: "list")
		}
		catch (DataIntegrityViolationException e) {
			flash.message = message(code: 'default.not.deleted.message', args: [fileEntryInstance.fileName, id])
			redirect(action: "list")

		}
	}

リクエストパラメータparamsの中に含まれるdeleteKeyのSHA-1ハッシュ値を計算し、それがDBに保存されているdeleteKeyと一致すれば削除を行います。削除できなかった場合には、エラーメッセージをflash.messageに格納して、list.gspの画面を表示します。

delete アクションは引数 Long id を持っていますが、ここにはリクエストパラメータ params に含まれる params.id が代入された状態で実行されます。


GSPの編集 / list.gsp

Grailsのビューは、GSP (Groovy Server Pages)
を用いて記述します。ここでは、Scaffoldにより自動生成したGSPを改造していきます。

まず、アップロードしたファイルの一覧画面であるlist.gspを編集してみましょう。

<td>${fieldValue(bean: fileEntryInstance, field: "fileName")}</td>

<td>${fieldValue(bean: fileEntryInstance, field: "fileComment")}</td>

<td>${fieldValue(bean: fileEntryInstance, field: "fileSize")}</td>

<td><g:formatDate date="${fileEntryInstance.dateCreated}" /></td>

<td>${fieldValue(bean: fileEntryInstance, field: "downloadKey")}</td>

<td>${fieldValue(bean: fileEntryInstance, field: "deleteKey")}</td>

となっているところを、以下のように変更します。

となっているところを、以下のように変更します。

<td>${fieldValue(bean: fileEntryInstance, field: "fileName")}</td>

<td>${fieldValue(bean: fileEntryInstance, field: "fileComment")}</td>

<td>${fieldValue(bean: fileEntryInstance, field: "fileSize")}</td>

<td><g:formatDate date="${fileEntryInstance.dateCreated}" format="yyyy/MM/dd HH:mm:ss" /></td>

<td>
	
		<g:field type="password" name="downloadKey" size="10" />
		<g:hiddenField name="id" value="${fileEntryInstance.id}" /><br>
		<g:submitButton name="download" class="save" value="${message(code: 'default.button.download.label', default: 'Download')}" />
	
</td>

<td>
	
		<g:field type="password" name="deleteKey" size="10" />
		<g:hiddenField name="id" value="${fileEntryInstance.id}" /><br>
		<g:submitButton name="delete" class="save" value="${message(code: 'default.button.delete.label', default: 'Delete')}" />
	
</td>

fileNameを表示している部分に g:link タグで show アクション(show.gsp)に遷移するリンクが貼られていますが、これは今回不要なので削除します。

dateCreatedを表示している部分では、g:formatDateタグのformat="yyyy/MM/dd HH:mm:ss"で表示形式を指定しています。この指定を行わなずデフォルトのまま表示させると、日時の末尾にタイムゾーンも表示します。ビューで個別にformatを指定する以外に、messages_ja.propertiesの設定値default.date.formatを変更し、アプリケーション全体の表示方法を変更する方法もあります。

downloadKey と deleteKey については、それぞれ download アクションと delete アクションを呼び出すフォームを設置し、入力フィールド g:field と送信ボタン g:submitButton を表示します。また、コントローラに対象となるインスタンスの id を渡すために、g:hiddenField を利用しています。

fileNameを表示している部分に g:link タグで show アクション(show.gsp)に遷移するリンクが貼られていますが、これは今回不要なので削除します。

dateCreatedを表示している部分では、g:formatDateタグのformat="yyyy/MM/dd HH:mm:ss"で表示形式を指定しています。この指定を行わなずデフォルトのまま表示させると、日時の末尾にタイムゾーンも表示します。ビューで個別にformatを指定する以外に、messages_ja.propertiesの設定値default.date.formatを変更し、アプリケーション全体の表示方法を変更する方法もあります。

downloadKey と deleteKey については、それぞれ download アクションと delete アクションを呼び出すフォームを設置し、入力フィールド g:field と送信ボタン g:submitButton を表示します。また、コントローラに対象となるインスタンスの id を渡すために、g:hiddenField を利用しています。

GSPの編集 / _form.gsp

ファイルをアップロードする画面を編集しましょう。ここでは、表示されるフォームを調整したいので、
_form.gspを編集します。

以下のようにfileNameとfileSizeの入力フォームが定義されていますが、アップロードされたファイルの持つ情報を用いてコントローラ(FileEntryControllerのsaveアクション)で設定しているので不要です。それぞれコメントアウトするか削除しましょう。GSP上でコメントアウトするには <%-- --%>を使います。

<div class="fieldcontain ${hasErrors(bean: fileEntryInstance, field: 'fileName', 'error')} ">
	<label for="fileName">
		<g:message code="fileEntry.fileName.label" default="File Name" />
		
	</label>
	<g:textField name="fileName" value="${fileEntryInstance?.fileName}"/>
</div>
<div class="fieldcontain ${hasErrors(bean: fileEntryInstance, field: 'fileSize', 'error')} ">
	<label for="fileSize">
		<g:message code="fileEntry.fileSize.label" default="File Size" />
		
	</label>
	<g:field name="fileSize" type="number" value="${fileEntryInstance.fileSize}"/>
</div>

URLマッピングとメッセージリソース

URLマッピングの変更 / UrlMappings.groovy

$ grails run-app

のコマンドで起動し、http://localhost:8080/uploader/ にアクセスすると、次のような画面が表示されます。

< UrlMappings変更前のインデックス画面 >

UrlMappings変更前のインデックス画面

これを、アップロードされたファイルの一覧画面に変更してみましょう。そのために、UrlMappingsの"/"に対する記述を

"/"(view:"/index")

から

"/"(controller:"fileEntry", action:"list")

に変更します。この状態で http://localhost:8080/uploader/ にアクセスすると、

< UrlMappings変更後のインデックス画面 >

UrlMappings変更後のインデックス画面

のような表示に変わりましたね。

メッセージリソース / messages_ja.properties

ここまでで、ファイルアップローダのアプリケーションとしては、ひととおりの機能が動くようになりました。最後に、少し画面に表示される文言をカスタマイズして、よりファイルのアップローダーらしくしてみましょう。grails-app/i18n/messages_ja.propertiesの末尾に、以下の記述を追加してみてください。

fileEntry.fileName.label=ファイル名
fileEntry.fileComment.label=コメント
fileEntry.fileSize.label=サイズ
fileEntry.dateCreated.label=日時
fileEntry.downloadKey.label=ダウンロードキー
fileEntry.deleteKey.label=削除キー
fileEntry.fileData.label=ファイル

fileEntry.list.label=ファイル一覧
fileEntry.create.label=ファイルのアップロード
fileEntry.new.label=新しくファイルをアップロード

default.button.download.label=ダウンロード

また、list.gspのファイルの冒頭付近のいくつかの行を変更します。ページのタイトルを表示する部分


のcodeを、↓に変更します。


「FileEntryを新規作成」のリンクが表示されている部分

			<h1><g:message code="default.list.label" args="[entityName]" /></h1>

のcodeを、↓に変更します。

				<li><g:link class="create" action="create"><g:message code="fileEntry.new.label" /></g:link></li>

「FileEntryリスト」のタイトルが表示されている部分

			<h1><g:message code="default.list.label" args="[entityName]" /></h1>

の code を、↓に変更します。

			<h1><g:message code="fileEntry.list.label" /></h1>

この状態で http://localhost:8080/uploader/ にアクセスすると、ファイルの一覧画面のメッセージが変わっていることが分かります。

< メッセージリソース変更後のファイル一覧画面 >

メッセージリソース変更後のファイル一覧画面

同様に、create.gspも変更してみましょう。ページのタイトルを表示する部分

		<title><g:message code="default.create.label" args="[entityName]" /></title>

の code を、↓に変更します。

		<title><g:message code="fileEntry.create.label" /></title>

「FileEntryリスト」のリンクが表示されている部分

				<li><g:link class="create" action="create"><g:message code="default.list.label" args="[entityName]" /></g:link></li>

の code を、↓に変更します。

				<li><g:link class="create" action="create"><g:message code="fileEntry.list.label" /></g:link></li>

「FileEntryリスト」のタイトルが表示されている部分

			<h1><g:message code="default.create.label" args="[entityName]" /></h1>

の code を、↓に変更します。

			<h1><g:message code="fileEntry.create.label" /></h1>

< メッセージリソース変更後のファイルアップロード画面 >

メッセージリソース変更後のファイルアップロード画面

こちらも、ファイルのアップローダとして違和感のないメッセージで表示されるようになりましたね。もしあなたが、このアプリケーションをWeb上で公開しようとするなら、不要なコントローラアクションやGSPを削除し、1ファイルの容量の上限を設けたり、アップロードされたファイル全体の容量に応じて古いファイルを削除したりする機能を追加すると良いでしょう。

おわりに

これで今回の紹介は終わりです。Grailsを用いて、手軽にWebアプリケーションを構築していく流れが体験できたでしょうか?この記事が、あなたのこれからのGrailsライフの一助になることを願っています。