GAEでSpringMVCを動かしてみる4(続 Spring Security を試してみる)

前回のエントリー(GAEでSpringMVCを動かしてみる3(Spring Security を試してみる) | Walk on apps.)からの続きです。

SpringSecurityを利用して、ログイン・ログアウト機能を試しています。
前回は、「とりあえず、まずは動かしてみる、」ってことをやってみました。
今回は、パスワード暗号化や、ユーザごとのアクセス制御、HTTPS対応、とかを追加して「もうちょっと、きちんと動かしてみる」、つもりです。

前回のエントリーまでは、ソースコードをBlog上にまるごと貼り付けていたんですが、大変なのでGitHubで公開することにしました。
GitHubからDownload

Blogでは、ポイントに絞って書いていきます。


(6)もうちょっと、きちんと動かしてみる
次のようなことを、設定してみようと思います。
・パスワードの暗号化
・アクセス制御
・ログインユーザ名の取得
・オリジナルのLoginページの作成
・Logout時の設定
・通信の暗号化(HTTPS設定)
・日本語の文字化け対応

・パスワードの暗号化
StandardPasswordEncoderを利用して、パスワードを暗号化します。SpringSecurityのチュートリアルを参考にしました。「Using encoded passwords」のところに設定例が載っています。
Spring Security — Tutorial

マニュアルによると、StandardPasswordEncoderは、SHA-256でハッシュ化を行うようです。あと、saltとして、ランダムな8Byteデータを付与してくれる。
25. Spring Security Crypto Module

SpringSecurityのBean定義ファイルには、以下のように書きました。ユーザを2つ(admin, bob)定義しています。パスワードは、ユーザ名と同じにしています。

spring-security-dispatcher.xml への追記内容

    <beans:bean id="encoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>

    <authentication-manager>
        <authentication-provider>
            <password-encoder ref="encoder"/>
            <user-service>
                <user name="admin" password="d4c4f5f190abf98b0d5224b549a1f2645acf975c75ec5d2d51895b0a7ca35949fc33576cc8294769" authorities="supervisor, user" />
                <user name="bob" password="cb60b076bebc371a3e5f6097399a82eb65948513639f4b090e70e2d49b6808fa3b02aa9bcd25668f" authorities="user" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

ハッシュ値の計算方法は、StandardPasswordEncoderのencodeメソッドを使います。チュートリアルでは、scalaのインタプリタを使っていますが、僕は地道にJavaのコードを書きました。以下のようにして、ハッシュ値を計算できます。

File名:PasswordEncode.java

import org.springframework.security.crypto.password.StandardPasswordEncoder;

public class PasswordEncode {
        public static void main(String[] args) throws Exception {
            StandardPasswordEncoder encoder = new StandardPasswordEncoder();
            System.out.println(encoder.encode("admin"));
            System.out.println(encoder.encode("bob"));
        }
}

・アクセス制御
SpringSecurityのBean定義ファイルで、use-expressions=”true”の設定をして、アクセス制御の設定をします。
これも、SpringSecurityのチュートリアルを参考にしました。
Spring Security — Tutorial

以下のような権限を、設定してみることにしました。
・index.htmlと、ログイン・ログアウト画面には、全員に許可。
・コンテキストパス「/velocity/admin」はadminだけ許可。
・コンテキストパス「/velocity/user」は、admin, bobに許可。
・その他のパスは、アクセス禁止。

spring-security-dispatcher.xml への追記内容は、こんな感じ。

<http use-expressions="true">
    <intercept-url pattern="/index.html" access="permitAll" />
    <intercept-url pattern="/velocity/login" access="permitAll"/>
    <intercept-url pattern="/velocity/loginfailed" access="permitAll" />
    <intercept-url pattern="/velocity/logout" access="permitAll" />

    <intercept-url pattern="/velocity/admin/**" access="hasRole('supervisor')" />
    <intercept-url pattern="/velocity/user/**" access="isAuthenticated()" />
    <intercept-url pattern="/**" access="denyAll" />

   :    :
   (中略)

</http>

access=”permitAll”といった、権限の書き方一覧は、以下のマニュアルの「16.1.1 Common Built-In Expressions」「16.2 Web Security Expressions」に載っていました。
16. Expression-Based Access Control
上記マニュアルによると、permitAllといった書き方は、WebSecurityExpressionRootクラスで定義されているとのこと。一応JavaDocも見てみた。
WebSecurityExpressionRoot (Spring Security 3.2.0.M1 API)

・ログインユーザ名の取得
現在ログインしているユーザ名の取得を行い、ブラウザ画面に表示できるようにします。
Controllerのコードを修正します。こちらのサイトを参考にしました。
Get current logged in username in Spring Security

ControllerのメソッドにPrincipalインターフェースを追加して、それを利用してユーザ名を取得しています。
修正ファイル:VelocityController.java

    (中略)
   :    :

@Controller
@RequestMapping("/velocity")
public class VelocityController {

    //DI via Spring
    String message;

    public void setMessage(String message) {
        this.message = message;
    }

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model, Principal principal) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        String name = principal.getName(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }

   :    :
   (中略)

参考にしたサイトによると、ほかにも2つやり方があるらしく、ちょっと書き方変えるだけなので、参考に載せておきます。

まず、「Authentication.getName()」を使うパターン。ControllerのメソッドにPrincipalインターフェースを追加しなくても動きます。

   :  (中略)  :
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
   :  (中略)  :

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String name = auth.getName(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }
   :    :
   (中略)

次に「User.getUsername()」を使うパターン。こちらも、ControllerのメソッドにPrincipalインターフェースを追加しなくても動きます。

   :  (中略)  :

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
   :  (中略)  :

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String name = user.getUsername(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }
   :    :
   (中略)

・オリジナルのLoginページの作成
こちらのサイトを参考にしました。
Spring Security form login using database
このサイトからダウンロードしたサンプルに、ログイン画面のサンプルがあります。

まず、SpringSecurityのBean定義ファイルに、form-login設定をします。

login-pageでは、ログイン画面を指定します。authentication-failure-urlでは、認証エラー時に表示する画面を指定します。
default-target-urlは、リダイレクト先が指定されていないときに、ログイン後に表示する画面を指定します。

認証が必要な画面/velocity/userにアクセスするとログイン画面/velocity/loginが表示されます。ログイン完了すると /velocity/userにリダイレクトされます。ですが、ログイン画面/velocity/loginに直接アクセスする場合もあります。このとき は、ログイン完了後にリダイレクトする宛先がありません。こういったときに、default-target-urlで指定された画面が表示されます。

spring-security-dispatcher.xml への追記内容

<http use-expressions="true">
   :  (中略)  :
    <form-login login-page="/velocity/login" default-target-url="/index.html"
        authentication-failure-url="/velocity/loginfailed" />
   :  (中略)  :
</http>

次に、Controllerのメソッドにログイン画面への遷移を追加します。
ログイン画面/velocity/loginと、認証エラー画面/velocity/loginfailedは、同じHTMLテンプレートを用いています。認証エラー時は、error変数にtrueを設定し、テンプレートに返しています。

修正ファイル:VelocityController.java

:  (中略)  :
@Controller
@RequestMapping("/velocity")
public class VelocityController {

:  (中略)  :
    @RequestMapping(value="/login", method = RequestMethod.GET)
    public String login(ModelMap model) {

        return "login";
    }

    @RequestMapping(value="/loginfailed", method = RequestMethod.GET)
    public String loginerror(ModelMap model) {

        model.addAttribute("error", "true");
        return "login";
    }
:  (中略)  :

}

次にログイン画面のテンプレートを作成します。
認証エラー時に、SPRING_SECURITY_LAST_EXCEPTION.messageにエラー内容が入るみたいなので、認証エラー時はそれを表示するようにしています。

作成したFile:login.vm

<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Login Page</title>

<style>
.errorblock {
    color: #ff0000;
    background-color: #ffEEEE;
    border: 3px solid #ff0000;
    padding: 8px;
    margin: 16px;
}
</style>

</head>

<body onload='document.f.j_username.focus();'>
    <h3>Login with Username and Password ログインテスト</h3>

    #if(${error}=="true")
        <div>
            Your login attempt was not successful, try again.<br /> Caused :
            $SPRING_SECURITY_LAST_EXCEPTION.message
        </div>
    #end

    <form name='f' action="j_spring_security_check"
        method='POST'>

        <table>
            <tr>
                <td>User:</td>
                <td><input type='text' name='j_username' value=''>
                </td>
            </tr>
            <tr>
                <td>Password:</td>
                <td><input type='password' name='j_password' />
                </td>
            </tr>
            <tr>
                <td colspan='2'><input name="submit" type="submit"
                    value="submit" />
                </td>
            </tr>
            <tr>
                <td colspan='2'><input name="reset" type="reset" />
                </td>
            </tr>
        </table>

    </form>
</body>
</html>

「認証エラー時に、SPRING_SECURITY_LAST_EXCEPTION.messageにエラー内容が入る」と書きましたが、その値がApacheVelocity側に引き渡されるように、VelocityViewResolverクラスへの設定追加が必要です。

Bean定義ファイルspring-security-dispatcher.xml の設定修正し、exposeSessionAttributesをtrueに設定します。

   :  (中略)  :
    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType" value="text/html;charset=UTF-8" />
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
        <property name="exposeSessionAttributes" value="true"/>
    </bean>
   :  (中略)  :

・Logout時の設定

まず、SpringSecurityのBean定義ファイルに、logout設定をします。

logout-success-urlで、ログアウト画面を指定します。delete-cookiesでログアウト時にJSESSIONIDをcookieから削除するようにしています。

spring-security-dispatcher.xml への追記内容

<http use-expressions="true">
:  (中略)  :
    <logout logout-success-url="/velocity/logout" delete-cookies="JSESSIONID"/>

</http>

次に、Controllerのメソッドにログアウト画面への遷移を追加します。

修正ファイル:VelocityController.java

:  (中略)  :
@Controller
@RequestMapping("/velocity")
public class VelocityController {

    //DI via Spring
    String message;

    public void setMessage(String message) {
        this.message = message;
    }

:  (中略)  :

    @RequestMapping(value="/logout", method = RequestMethod.GET)
    public String logout(ModelMap model) {

        model.addAttribute("message", this.message);
        return "logout";
    }

}

次にログアウト画面のテンプレートを作成します。

作成したFile:logout.vm

<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Hello App Engine</title>
  </head>

  <body>

    message: ${message}<br/>
    <p>Logoutしました。</p>
  </body>
</html>

・通信の暗号化(HTTPS設定)
SpringSecurityで特定のURLに対してHTTPSを要求する設定ができます。ただし、HTTPS自体をSpringSecurityで制御するわけでないので、HTTPSの入り口は、APサーバ等で用意しておく必要があります。

そのため、まず、GoogleAppEngine側でHTTPSの入り口を用意する必要があります。GAEでHTTPSを使う場合、web.xmlにsecurity-constraint設定を追加すると利用できます。
配備記述子: web.xml – Google App Engine — Google Developers

注意点としては、Google App Engine SDKでは、開発環境ではHTTP接続、GAE上ではHTTPS接続になります。
SpringSecurityでHTTPSを要求する設定をした場合、開発環境でもHTTPSが使えるようにしておかないと、アクセスができません。そのため、僕の場合、HTTPS関連設定はGAE側にすべて任せることにします。

web.xmlに、security-constraint設定を追加します。

  :  (中略)  :
    <!-- GAE Setting -->
    <security-constraint>
        <web-resource-collection>
            <url-pattern>/velocity/login*</url-pattern>
            <url-pattern>/velocity/loginfailed*</url-pattern>
            <url-pattern>/velocity/logout*</url-pattern>
        </web-resource-collection>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
  :  (中略)  :

参考までに、SpringSecurityで設定する場合の内容を書いておきます。
マニュアルはこちら。3. Security Namespace Configuration

requires-channelでプロトコルを指定すると設定できます。
port-mappingは必要に応じて設定します。開発環境などで、通常のポートが使えないときに指定します。

  <http>
    <intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/>
    <intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/>
    ...

    <port-mappings>
      <port-mapping http="9080" https="9443"/>
    </port-mappings>
    ...
  </http>

・日本語の文字化け対応
HTMLテンプレートに日本語があると、ブラウザで表示させたときに、文字化けがおきます。
そのため、Bean定義ファイルと、web.xmlに設定追加しておく必要があります。
こちらのサイトを参考にしました Spring-MVCのビューにVelocityを使いたい – PukiWiki

今回はUTF-8に統一することにします。
まず、Bean定義ファイルspring-security-dispatcher.xmlに設定追加します。

VelocityConfigurerクラスのinput.encoding、output.encodingにUTF-8を設定。
→ たぶん、テンプレートファイル読込と、HTML出力の設定だと思う

VelocityViewResolverクラスの、contentTypeにUTF-8を設定。
→ ブラウザに返却するContent-Typeを設定してる。HTTPヘッダですかね。

 
   :  (中略)  :
    <bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/WEB-INF/view/"/>

        <property name="velocityProperties">
            <props>
            <prop key="input.encoding">UTF-8</prop>
            <prop key="output.encoding">UTF-8</prop>
            </props>
        </property>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType" value="text/html;charset=UTF-8" />
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
        <property name="exposeSessionAttributes" value="true"/>
    </bean>
   :  (中略)  :

次にweb.xmlに設定追加します。

CharacterEncodingFilterクラスで、encodingにUTF-8を設定します。
→HTMLフォームからの入力された文字を、UTF-8にしてサーバ側で処理することになります。

   :  (中略)  :
    <!-- UTF-8 Setting -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
   :  (中略)  :

上記設定には、forceEncodingというパラメータもあるみたいだけど、今回は設定しませんでした。

   :  (中略)  :
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
   :  (中略)  :

(7)アプリ起動
GAE上で動かしてみました。
コンテキストパス/velocity/user にhttpでアクセスすると、ログイン画面にリダイレクトされます。httpsになってます。
spring4_01

bobでログインすると、画面が返ってきました。httpにもどっています。
ユーザ名bobが、画面に表示されています。
spring4_02

bobユーザのままで、/velocity/admin にアクセスすると、アクセス拒否されます。
spring4_03

/velocity/user 画面でログアウトをクリックすると、ログアウトします。httpsになっています。
spring4_04

ログイン画面でパスワードを間違えると、エラーメッセージが表示されます。
spring4_05


Advertisements

GAEでSpringMVCを動かしてみる3(Spring Security を試してみる)

GoogleAppEngineの勉強中です。
前回のエントリーで、SpringMVCにテンプレートエンジンのApache Velocityを組み合わせて動かしてみました。
今回は、SpringSecurityを利用して、ログイン・ログアウト機能を試してみます。

とりあえず、ユーザの管理は設定ファイルに書き込んでおくシンプルな形を試します。(ユーザデータをDBに保存したり、外部の認証サービス(GoogleAccountとかTwitterとか)を利用する、みたいなことはやらない。)

————————————————————–
使っている環境
Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1

Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

Spring Framework 3.2.2.RELEASE
Commons Collections 3.2.1
Commons Lang 2.6
Commons Logging 1.1.2
Apache Velocity 1.7
Spring Security 3.2.0.M1


(1)はじめに
今回はSpring Securityの機能を使って、ユーザ認証とアクセス制御を試してみます。
「SpringMVC + Apache Velocity」については、過去のエントリーを参照してください。
GAEでSpringMVCを動かしてみる | Walk on apps.
GAEでSpringMVCを動かしてみる2(テンプレートエンジンVelocityを組込む) | Walk on apps.

進め方としては、「とりあえず、まずは動かしてみる、」ってことをやってみます。
そのあと、パスワード暗号化や、ユーザごとのアクセス制御、HTTPS対応、とかを追加して「もうちょっと、きちんと動かしてみる」、つもりです。


(2)SpringFramework、Commons Loggingのダウンロード
こちらについては、前回のエントリーに書いてあるのでそちらを参照してください。
GAEでSpringMVCを動かしてみる | Walk on apps.


(3)velocity関連ライブラリのダウンロード
以下の3種類必要になります。
Apache Velocity
Commons Collections
Commons Lang

こちらについては、前回のエントリーに書いてあるのでそちらを参照してください。
GAEでSpringMVCを動かしてみる2(テンプレートエンジンVelocityを組込む) | Walk on apps.


(4)ライブラリ追加
ライブラリに追加するjarファイルは、以下の15個

commons-collections-3.2.1.jar
commons-lang-2.6.jar
commons-logging-1.1.2.jar
spring-aop-3.2.2.RELEASE.jar
spring-beans-3.2.2.RELEASE.jar
spring-context-3.2.2.RELEASE.jar
spring-context-support-3.2.2.RELEASE.jar
spring-core-3.2.2.RELEASE.jar
spring-expression-3.2.2.RELEASE.jar
spring-security-config-3.2.0.M1.jar
spring-security-core-3.2.0.M1.jar
spring-security-web-3.2.0.M1.jar
spring-web-3.2.2.RELEASE.jar
spring-webmvc-3.2.2.RELEASE.jar
velocity-1.7.jar

これらのjarファイルを、Eclipseの“war/WEB-INF/lib”フォルダに、配置します。
また、Eclipseの、「Java Build Path」に追加します。


(5)まずは動かしてみる。
Springのマニュアルと、
3. Security Namespace Configuration

こちらのサイトを参考にしました。
Spring Security hello world example
Spring Security logout example

まずは、Controllerを作成します。
リクエスト「/velocity/***」に対して、テンプレートsample.vmを返す想定にしています。
テンプレートの置き場所や、ファイル名の拡張子については、SpringのBean定義ファイルの中で定義しています。

File名:src/sample/controller/VelocityController.java

package sample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/velocity")
public class VelocityController {

    //DI via Spring
    String message;

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }

    public void setMessage(String message) {
        this.message = message;
    }

}

次に、テンプレート(.vmファイル)を作成します。

リクエスト「/velocity/***」を受け取ったControllerが、***の部分を抜き出して、path変数に入れて、テンプレートに引き渡しています。

message変数は、SpringのBean定義ファイルの中でControllerに対して初期設定しています。Controllerがその値をテンプレートに引き渡しています。

File名:war/WEB-INF/view/sample.vm

<html>
<body>
path: ${path}<br/>
message: ${message}<br/>
<p><a href="j_spring_security_logout">Logout</a></p>
</body>
</html>

次に、SpringのBean定義ファイルを作成します。
Beanの定義と、テンプレートの場所を指定しています。

File名:war/WEB-INF/spring-mvc-dispatcher-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.2.xsd
                        http://www.springframework.org/schema/mvc
                        http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
                        ">

    <mvc:annotation-driven />

    <bean class="sample.controller.VelocityController">
        <property name="message">
            <value>Velocity Sample</value>
        </property>
    </bean>

    <bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/WEB-INF/view/"/>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
    </bean>

</beans>

次に、Spring Securityの定義ファイルを作成します。
とりあえず、admin, bobというユーザを準備しておきました。パスワードは、ひとまず平文で書いておきます。

File名:war/WEB-INF/spring-security-dispatcher.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                        http://www.springframework.org/schema/security
                        http://www.springframework.org/schema/security/spring-security-3.1.xsd
                        ">

    <!-- Spring Security -->

    <http auto-config='true'>
        <intercept-url pattern="/**" access="ROLE_USER" />
    </http>

    <authentication-manager>
        <authentication-provider>
            <user-service>
              <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
              <user name="bob" password="bob" authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

</beans:beans>

次に、web.xmlを修正します。

すべてのコンテキストパスに対して、DelegatingFilterProxyを適用する(フィルタする?)ように設定しています。(赤字にしています)

contextConfigLocationのところで、Spring Securityの定義ファイルを指定しています。
ここでは、Bwan定義ファイル(spring-mvc-dispatcher-servlet.xml)については指定していません。これは、servlet-nameで指定している名称spring-mvc-dispatcherに紐づいた名前のファイルを、自動的に読み込みに行ってくれるから。(というか、名前を紐づけないやり方がわからない。。)

File名:war/WEB-INF/web.xml

<?xml version="1.0" encoding="utf-8" standalone="no"?><web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.5" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <!-- Spring MVC -->
    <servlet>
        <servlet-name>spring-mvc-dispatcher</servlet-name>
        <servlet-class>
                    org.springframework.web.servlet.DispatcherServlet
                </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/spring-security-dispatcher.xml
        </param-value>
    </context-param>

    <!-- Spring Security -->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
      <filter-name>springSecurityFilterChain</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>

</web-app>

appengine-web.xmlへ設定追加
GoogleAppEngineでは、デフォルトではセッションが使用できないようです。なので、appengine-web.xmlに以下の設定を追加します

<sessions-enabled>true</sessions-enabled>

作成したソースコードの配置場所をまとめると、こんな感じになっています。
spring_security01

自PC上で、ためしにアプリ起動
http://localhost:8888/velocity/test  にアクセスしてみます。ログイン画面が出てきました。
spring_security02

ユーザ「bob」でログインすると、画面が出ました!!
spring_security03

GAE上にDeployしても、きちんと動きました。よかった。


(6)もうちょっと、きちんと動かしてみる
:  :
:  :
記事が長くなってきたので、ここから先は次回のエントリーに書くことにします。
あと、ソースを張付けるのが大変になってきたので、GitHubを導入してみたいと思う!
こちら→ GAEでSpringMVCを動かしてみる4(続 Spring Security を試してみる) | Walk on apps.

GAEでSpringMVCを動かしてみる2(テンプレートエンジンVelocityを組込む)

GoogleAppEngineの勉強中です。
前回のエントリーで、SpringMVCを動かしてみました。
その時は、ひとまずjspで画面表示していたけど、今回は、テンプレートエンジンのApache Velocity を使って、「SpringMVC + Velocity」を試してみます。
とりあえず動かすことを目標にしているので、テンプレートの分割(ヘッダ、フッタ、コンテンツ、サイドバー領域とか)まではやりません。

————————————————————–
使っている環境
Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1

Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

Spring Framework 3.2.2.RELEASE
Commons Collections 3.2.1
Commons Lang 2.6
Commons Logging 1.1.2
Apache Velocity 1.7


(1)はじめに

テンプレートエンジンって、そもそも、どれ使えばいいのか謎。
このページに一覧が載ってるけど、すげーたくさんある。
Template engine (web) – Wikipedia, the free encyclopedia

今回は、Springと連携させる、ってことで、以下のサイトを参考にしました。
(a)SpringMVCで使えるテンプレートエンジンを比較してみた #jsug – from world import goodies
(b)Template Engineの比較(Java) – No Bugs, No Life

上記サイトを見た感じだと、
・Velocity
・Freemarker
・Thymeleaf
・Mayaa

といったものが候補になりそう。(JSPはテンプレートの分割ができないと思うので最初から除外(タグを使えばできる?))

また、SpringFrameworkのドキュメントの「18. View technologies」にテンプレートのことが書かれてあるのを見つけました。
その中の、「18.4. Velocity & FreeMarker」で、Velocity 、FreeMarkerの説明があるので、この2つのうちどちらかがよさそう。
18. View technologies

で、先述したサイト(b)に記載があるんですが、FreeMarkerについては、現時点の最新版FreeMarker 2.3.19に、セキュリティの問題があるらしい。
FreeMarker Manual – 2.3.19

Attention! This release contains two important security workarounds that unavoidably make it obvious how some applications can be exploited. FreeMarker can’t solve these issues on all configurations, so please read the details instead of just updating FreeMarker!

というわけで、Velocityを使うのが無難そう。


(2)SpringFramework、Commons Loggingのダウンロード
こちらについては、前回のエントリーに書いてあるのでそちらを参照してください。
GAEでSpringMVCを動かしてみる | Walk on apps.


(3)velocityのダウンロード
こちらのサイトのDownloadsから入手できます。
Apache Velocity Site – The Apache Velocity Project

僕の場合、velocity-1.7.tar.gz をダウンロードしました。


(4)Commons Collectionsのダウンロード
こちらのサイトのDownloadsから入手できます。
Collections – Home

僕の場合、commons-collections-3.2.1-bin.tar.gz をダウンロードしました。


(5)Commons Langのダウンロード
こちらのサイトのDownloadsから入手できます。
Lang – Home

Commons Langについては、ちょっと注意です。
Commons Langがない状態でSpringMVCを起動すると、Class Not Foundエラーが出ます。原因は以下のClassでした。
org.apache.commons.lang.StringUtils

が、現時点での最新版、commons-lang3-3.1を使うとこのエラーが解消しません。JavaDocを見ると
version 3.*ではクラス名が微妙に違っているみたいでした。
extended by org.apache.commons.lang3.StringUtils

なので、version2.*系の最新版、commons-lang-2.6-bin.tar.gz をダウンロードしました。


(6)ライブラリ追加
ライブラリに追加するjarファイルは、以下の11個
commons-collections-3.2.1.jar
commons-lang-2.6.jar
commons-logging-1.1.2.jar
spring-beans-3.2.2.RELEASE.jar
spring-context-3.2.2.RELEASE.jar
spring-context-support-3.2.2.RELEASE.jar
spring-core-3.2.2.RELEASE.jar
spring-expression-3.2.2.RELEASE.jar
spring-web-3.2.2.RELEASE.jar
spring-webmvc-3.2.2.RELEASE.jar
velocity-1.7.jar

これらのjarファイルを、Eclipseの“war/WEB-INF/lib”フォルダに、配置します。
spring_velocity01

また、Eclipseの、「Java Build Path」に追加します。
spring_velocity02


(7)コーディング

まずは、Controllerを作成します。
Controllerの書き方は、こちらのサイトを参考にしました。
Spring-MVCとVelocityを使った Twitterライクに REST風な URLハンドリングをするサイトのテンプレート

書き方としては、Velocityを使わなかった時と変わらない書き方で大丈夫そう。(今回は、新規作成したので、前回のエントリーで作成したものと若干ちがっています。)
おそらく、SpringMVCでは、テンプレートエンジンに依存しない書き方ができるように設計されているんだと思った。

今回は、リクエスト「/velocity/***」に対して、テンプレートsample.vmを返す想定にしています。
model.addAttributeメソッドでテンプレートに引き渡す値をセットし、return “sample”;でテンプレートを指定しています。
テンプレートの置き場所や、ファイル名の拡張子については、SpringのBean定義ファイルの中で定義しています。

File名:src/sample/controller/VelocityController.java

package sample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/velocity")
public class VelocityController {

    //DI via Spring
    String message;

    @RequestMapping(value="/{name}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String name, ModelMap model) {

        model.addAttribute("name", name);
        model.addAttribute("message", this.message);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }

    public void setMessage(String message) {
        this.message = message;
    }

}

次に、テンプレート(.vmファイル)を作成します。

リクエスト「/velocity/***」を受け取ったControllerが、***の部分を抜き出して、name変数に入れて、テンプレートに引き渡しています。

message変数は、SpringのBean定義ファイルの中でControllerに対して初期設定しています。Controllerがその値をテンプレートに引き渡しています。

File名:war/WEB-INF/view/sample.vm

<html>
<body>
name: ${name}<br/>
message: ${message}<br/>
</body>
</html>

次に、SpringのBean定義ファイルを作成します。
Bean定義ファイルについては、前回のエントリーで作成したものに追記し、不要な部分をコメントアウトしています。

<bean class=”sample.controller.MovieController”>の部分は、今回関係ないので読み飛ばしてください。

<bean class=”sample.controller.VelocityController“>のところで、コントローラの初期設定をしています。

「bean id=”velocityConfig“」と、「bean id=”viewResolver“」のところで、テンプレートファイルの置き場所・拡張子を指定しています。(複数のディレクトリを使い分けることは、できるんだろうか?)

File名:war/WEB-INF/mvc-dispatcher-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.2.xsd
                        http://www.springframework.org/schema/mvc
                        http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
                        ">

<!--
    <context:component-scan base-package="sample.controller">
        <context:exclude-filter type="regex"
            expression="sample.controller.Movie.*" />
    </context:component-scan>
-->

    <mvc:annotation-driven />

    <!-- Bean to show you Di in GAE, via Spring, also init the MovieController -->
    <bean class="sample.controller.MovieController">
        <property name="message">
            <value>Hello World</value>
        </property>
    </bean>

<!--
    <bean
       class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix">
            <value>/pages/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>
-->

    <bean class="sample.controller.VelocityController">
        <property name="message">
            <value>Velocity Sample</value>
        </property>
    </bean>

    <bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/WEB-INF/view/"/>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
    </bean>

</beans>

次に、web.xmlを修正します。
web.xmlは、前回のエントリーで作成したときと同じ状態です。

リクエストをすべて、DispatcherServletに渡す、って感じ。と、Bean定義ファイルの場所を指定
ContextLoaderListenerは・・・なんだろ。。。。

File名:war/WEB-INF/web.xml

<?xml version="1.0" encoding="utf-8" standalone="no"?><web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.5" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <servlet>
        <servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>
                    org.springframework.web.servlet.DispatcherServlet
                </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
    </context-param>

    <listener>
        <listener-class>
                    org.springframework.web.context.ContextLoaderListener
                </listener-class>
    </listener>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>

</web-app>

作成したソースコードの配置場所をまとめると、こんな感じになっています。

spring_velocity03


(8)アプリ起動
ブラウザから、アクセスするとこんな感じ。
spring_velocity04

リクエストで、/velocity/*** としたときの、***の部分が、
ブラウザ画面上で「name: ***」と表示されます。

上記の画面キャプチャは、自分のPC上(開発環境)のものですが、GAE上に実際にDeployしてもきちんと動きました。

GAEでSpringMVCを動かしてみる

GoogleAppEngineの勉強中です。
GAEでJavaを使って開発する場合、JSP,Servletが使えます。が、MVCモデルでアプリ作成しようとすると、何らかのFrameworkを使わないとダメっぽい。

というわけで、SpringMVCを試してみました。

————————————————————–
使っている環境
Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1

Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

Spring Framework 3.2.2.RELEASE
commons-logging-1.1.2


(1)はじめに
Javaのフレームワークというと、Struts、Springくらいしか聞いたことがなくって、たまたま目にしたPlayFrameworkというものが結構よさそうだと思ってた。ただPlayFrameworkは、まだ仕様がちょっと落ち着いてないみたい、う~ん。

で調べてみると、比較資料がありました。
Comparing JVM Web Frameworks – Devoxx France 2013

この資料は、すごい参考になったんだけど、スライド90枚に及ぶ長編スペクタクルなので、簡単にポイントを紹介します。

資料の40~44ページに、比較項目にポイント付けして、ランキングしたものがあります。
Grailsが1位になってるんですけど、どうなんでしょうか。Springは2位ですね。

57ページに、各種情報サイト?で紹介されているTop JVM Frameworkがのっています。選定基準は謎ですけど、有名どころ?と思われるFrameworkはこのあたりのものなんでしょうか。

69ページから、GoogleTrendでどんだけ話題になっているか、
73ページから、JobTrend(仕事で需要あるか)、
なんてことも紹介されています。

そして、最終的に筆者は、「俺に聞くな、自分で考えろ」と言っています(笑)。

Prioritize a list of goals that are important to your application.
・目的を一覧にして、優先順位をつける
Pick 3-4 frameworks and do a spike with each, developing the same application.
・3~4つのフレームワークをつかって、同じ機能のアプリ作成してみる。
Document and rank each framework against your list of goals.
・目的を一覧化し、それぞれの項目について各フレームワークに点数をつける
Calculate and choose!
・ランキングして、どれにするか選ぶ
Don’t be a Picker
・適当にチョイスするのは避ける


(2)SpringFrameworkのダウンロード
こちらのサイトの、一番下のところにProjectsという一覧があって、そこのSpring Frameworkのページに行きます。
SpringSource.org

spring_framework

そうすると、右のほうに、ダウンロード・ドキュメントへのリンクがあります。
僕の場合、spring-framework-3.2.2.RELEASE-dist.zip をダウンロードしました。


(3)Commons Loggingのダウンロード
Springを動かしてみるとわかるんですが、起動時にCommons LoggingがないとClass NotFoundエラーが出ます。
古いバージョンのSpringだと、SpringFrameworkのダウンロードすると、中にCommons Loggingのjarファイルも入ってたみたいですが、今は別になっているようです。

こちらのサイトの、Downloadから入手できます。
Commons Logging – Overview

僕の場合、commons-logging-1.1.2-bin.tar.gz をダウンロードしました。


(4)ライブラリ追加
とりあえず、超シンプルなアプリで試してみたいだけなので、必要最小限のjarファイルをライブラリに追加します。
追加するファイルはこちら7つ。

commons-logging-1.1.2.jar
spring-beans-3.2.2.RELEASE.jar
spring-context-3.2.2.RELEASE.jar
spring-core-3.2.2.RELEASE.jar
spring-expression-3.2.2.RELEASE.jar
spring-web-3.2.2.RELEASE.jar
spring-webmvc-3.2.2.RELEASE.jar

これらのjarファイルを、Eclipseの“war/WEB-INF/lib”フォルダに、配置します。
spring_library1

また、Eclipseの、「Java Build Path」に追加します。
spring_library2


(5)コーディング
こちらのサイトを参考にして、サンプル作成しました。
Google App Engine + Spring 3 MVC REST example
Getting Started With Spring MVC and Google App Engine | [Be el o ge]

まずは、Controllerを作成します。

Eclipseのsrcフォルダ配下に、パッケージsample.controllerを作成し、その中にMovieController.javaを作成します。
File名:src/sample/controller/MovieController.java

package sample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/movie")
public class MovieController {

    //DI via Spring
    String message;

    @RequestMapping(value="/{name}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String name, ModelMap model) {

        model.addAttribute("movie", name);
        model.addAttribute("message", this.message);

        //return to jsp page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "list";

    }

    public void setMessage(String message) {
        this.message = message;
    }

}

次に、JSPファイルを作成します。
File名:war/pages/list.jsp

<html>
<body>
    <h1>GAE + Spring 3 MVC REST example</h1>

    <h3>Movie : ${movie} , DI message : ${message}</h3>    
</body>
</html>

次に、SpringのBean定義ファイルを作成します。
File名:war/WEB-INF/mvc-dispatcher-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.2.xsd
                        http://www.springframework.org/schema/mvc
                        http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
                        ">

<!--
    <context:component-scan base-package="sample.controller">
        <context:exclude-filter type="regex"
            expression="sample.controller.Movie.*" />
    </context:component-scan>
-->

    <mvc:annotation-driven />

    <!-- Bean to show you Di in GAE, via Spring, also init the MovieController -->
    <bean class="sample.controller.MovieController">
        <property name="message">
            <value>Hello World</value>
        </property>
    </bean>

    <bean
       class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix">
            <value>/pages/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

</beans>

GoogleAppEngineでは、アプリ起動時にかかる時間を気にする必要があります。(今回はサンプルアプリなので気にしなくていいですが)

こちらの情報によると、
Optimizing Spring Framework for App Engine Applications – Google App Engine — Google

Springの「Component Scanning」は使わないほうがいいとのことです。
なので、context:component-scan タグを使わないようにしました。

ファイルの頭で、xmlの定義を読込ませているところ(以下の項目)
xmlns:context
xmlns:mvc
xsi:schemaLocation

は、漏れの無いように注意です。僕は「xmlns:mvc」を書き忘れていて、アプリ起動時にmvc-dispatcher-servlet.xmlの読込エラーが発生して悩まされました。

次に、web.xmlを修正します。
File名:war/WEB-INF/web.xml

<?xml version="1.0" encoding="utf-8" standalone="no"?><web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.5" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <servlet>
        <servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>
                    org.springframework.web.servlet.DispatcherServlet
                </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
    </context-param>

    <listener>
        <listener-class>
                    org.springframework.web.context.ContextLoaderListener
                </listener-class>
    </listener>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>

</web-app>

作成したソースコードの配置場所をまとめると、こんな感じになります。

spring_files


(6)アプリ起動
ブラウザから、アクセスするとこんな感じ。
spring_sample_view

リクエストで、/movie/*** としたときの、***の部分が、
ブラウザ画面上で「Movie : ***」と表示されます。

上記の画面キャプチャは、自分のPC上(開発環境)のものですが、GAE上に実際にDeployしてもきちんと動きました。よかった。


4/21追記
テンプレートエンジンにApache Velocityを使ってみました。
GAEでSpringMVCを動かしてみる2(テンプレートエンジンVelocityを組込む) | Walk on apps.

(GAEで日本語の形態素解析を試してみる)第3回 Kuromojiを使ってみる

GoogleAppEngineの勉強中です。
3回に分けて、GAEで日本語の形態素解析を試してみたことを書いています。
第1回 lucene-gosenを使ってみる
第2回 lucene-gosenのTokenFilterを試す
第3回 Kuromojiを使ってみる

今回は、第3回です。

使っている環境
Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1

Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

kuromoji-0.7.7


(1)はじめに
もともと、kuromoji に関しては、GoogleAppEngineで動かせないと思っていました。
なぜかというと、GoogleAppEngineにアップロードするファイルサイズには、10MBまでという制限があるから。
kuromojiの場合、Jarファイルのサイズが、11MBなので、これだと制限にひっかかるな~、と思っていました。

だけど、Eclipse上でアプリのwarフォルダの中を見ていたら、libフォルダの中に入っているライブラリ(jarファイル)の中に、サイズが20MB超えてるものがあった。
あれ、もしかしてjarファイルの場合、10MB超えていても大丈夫なのかな、って思って、kuromojiを試してみたら、動いた!


(2)kuromojiダウンロード
こちらのサイトに行くと、
kuromoji – japanese morphological analyzer

「Downloading」というところに、GitHubへのリンクがあるので、それをクリック。
kuromoji-0.7.7.tar.gzをダウンロードし、展開します。中にlibフォルダがあり、その中にkuromoji-0.7.7.jarがあります。

このjarファイルをクラスパスに通しておけば、準備完了。


(3)コーディングの流れ
第2回にもかいたんだけど、まずはじめに、注意点です。
僕はGoogleAppEngine上で動かしています。
で、今回テスト用に作成するものは、HTTPのGETリクエストに「q=解析したい文章」といったパラメータを付けることで、パラメータqに設定した文章を解析して、ブラウザに結果を返すように作っています。

そのため、以下のようにして、getリクエストのパラメータqを取得して、変数contentに格納するようにしています。

String content = req.getParameter("q");

また、結果をブラウザに返却するため、以下のメソッドで出力しています。

resp.getWriter().println("出力内容をここに書く");

このあたりはGAEならではの作法になります。

それを踏まえて、kuromojiの使い方をみていきます。

まず、一番シンプルなコーディングのサンプルは、kuromojiのサイトに載っています。
kuromoji – japanese morphological analyzer

また、その他もろもろの情報は以下のサイトを参考にしました。
(参考)Java製形態素解析器「Kuromoji」を試してみる
(参考)sample/kuromoji-sample/src/main/java/jp/mwsoft/sample/kuromoji/SimpleTokenizerSample.java at master · mwsoft/sample · GitHub
(参考)sample/kuromoji-sample/src/main/java/jp/mwsoft/sample/kuromoji/TokenizerModeSample.java at master · mwsoft/sample · GitHub

以下のような形で、Tokenizerクラスのtokenizeメソッド実行時に、形態素解析されます。(ここでは、contentに解析したい文章を入れています)

        Builder builder = Tokenizer.builder();
        Tokenizer tokenizer = builder.build();
        List<Token> tokensNormal = tokenizer.tokenize(content);

上記では、解析結果がtokensNormalの中に入っていて、取り出すときは以下のような感じ。

        String words="";

        for (Token token : tokensNormal){
            words = words +
                    token.getSurfaceForm() + " | "; 
        }

        words = words + "\n";

        resp.getWriter().println(words);

TokenクラスのgetSurfaceFormメソッドで、解析された単語を取出せます。

上記の場合、たとえば「日本経済新聞でモバゲーの記事を読んだ。」という文章を解析した結果が、
「日本経済新聞 | で | モバゲー | の | 記事 | を | 読ん | だ | 。 |」

といった感じで表示される。

Tokenクラスには、ほかにも情報取得用のメソッドが用意されていて、以下のようにすると、いろいろ取得できる。

String words="";

for (Token token : tokens){
    words = words +
            "--------------------------------------------------" + "\n" + 
            "allFeatures : " + token.getAllFeatures() + "\n" + 
            "partOfSpeech : " + token.getPartOfSpeech() + "\n" + 
            "position : " + token.getPosition() + "\n" + 
            "reading : " + token.getReading() + "\n" + 
            "SurfaceForm : " + token.getSurfaceForm() + "\n" + 
            "allFeaturesArray : " + Arrays.asList(token.getAllFeaturesArray()) + "\n" + 
            "辞書にある言葉? : " + token.isKnown() + "\n" + 
            "未知語? : " + token.isUnknown() + "\n" + 
            "ユーザ定義? : " + token.isUser() + "\n"; 
}
resp.getWriter().println(words);

この場合、取得結果はこんな感じで表示されます。

--------------------------------------------------
allFeatures : 名詞,固有名詞,組織,*,*,*,日本経済新聞,ニホンケイザイシンブン,ニホンケイザイシンブン
partOfSpeech : 名詞,固有名詞,組織,*
position : 0
reading : ニホンケイザイシンブン
SurfaceForm : 日本経済新聞
allFeaturesArray : [名詞, 固有名詞, 組織, *, *, *, 日本経済新聞, ニホンケイザイシンブン, ニホンケイザイシンブン]
辞書にある言葉? : true
未知語? : false
ユーザ定義? : false
--------------------------------------------------
allFeatures : 助詞,格助詞,一般,*,*,*,で,デ,デ
partOfSpeech : 助詞,格助詞,一般,*
position : 6
reading : デ
SurfaceForm : で
allFeaturesArray : [助詞, 格助詞, 一般, *, *, *, で, デ, デ]
辞書にある言葉? : true
未知語? : false
ユーザ定義? : false
--------------------------------------------------
    :   :   :
    ( 中略 )

getAllFeaturesメソッドで取得できる情報ですが、
kuromojiでは、MeCab-IPADIC辞書を使っているようで、MeCabの出力フォーマットになっているようです。

品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音

MeCabのフォーマットは、以下のサイトの「使い方」-「とりあえず解析してみる」のところに載っています。
MeCab: Yet Another Part-of-Speech and Morphological Analyzer

さて、kuromojiには3つのモードが用意されています。
kuromojiのホームページによると、

・Normal – regular segmentation
・Search – use a heuristic to do additional segmentation useful for search
・Extended – similar to search mode, but also unigram unknown words (experimental)

Searchモードでは、「日本経済新聞」といった単語が、「日本, 経済, 新聞」といった感じに細かく分割されます。
Lucene/Solrで全文検索するときに、「 ”日本”という検索ワードで検索したときに、”日本経済新聞”も引っかかる」 ようになるみたいです。

Extendedモードでは、「辞書に登録されていない単語については、unigramで解析されて、1文字ずつ分割されます。
たとえば、「Lucene」という単語は、「L, u, c, e, n, e」と分割されます。

で、モードの切り替えは、builder.build()する前に、builder.mode()でモードをセットします。(以下では、tokenizerを使いまわしています)

        Builder builder = Tokenizer.builder();

        // Normalモードで解析
        builder.mode(Mode.NORMAL);
        Tokenizer tokenizer = builder.build();
        List<Token> tokensNormal = tokenizer.tokenize(content);

        // Searchモードで解析
        builder.mode(Mode.SEARCH);
        tokenizer = builder.build();
        List<Token> tokensSearch = tokenizer.tokenize(content);

        // Extendsモードで解析
        builder.mode(Mode.EXTENDED);
        tokenizer = builder.build();
        List<Token> tokensExtended = tokenizer.tokenize(content);

ここまでの内容を踏まえて、次に、作成したサンプルのソースを書いておきます。


(4)ソースコード
実際に作成したサンプルアプリのソースコードと、動かした出力結果を書いておきます。
まずは、ソースコードから。

package sample;

import java.io.*;
import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.atilika.kuromoji.Token;
import org.atilika.kuromoji.Tokenizer;
import org.atilika.kuromoji.Tokenizer.Builder;
import org.atilika.kuromoji.Tokenizer.Mode;

@SuppressWarnings("serial")
public class TestKuromoji_Servlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {

        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/plain; charset=UTF-8");
        resp.getWriter().println("TestKuromoji テスト");

        // パラメータ取得
        String content = req.getParameter("q");
        if (content == null || content.length() == 0){
            content = "";
        }

        resp.getWriter().printf("content= %s%n", content);

        Builder builder = Tokenizer.builder();

        // Normalモードで解析
        builder.mode(Mode.NORMAL);
        Tokenizer tokenizer = builder.build();
        List<Token> tokensNormal = tokenizer.tokenize(content);

        // Searchモードで解析
        builder.mode(Mode.SEARCH);
        tokenizer = builder.build();
        List<Token> tokensSearch = tokenizer.tokenize(content);

        // Extendsモードで解析
        builder.mode(Mode.EXTENDED);
        tokenizer = builder.build();
        List<Token> tokensExtended = tokenizer.tokenize(content);

        // 解析結果の表示(SurfaceFormのみ)
        resp.getWriter().println("NORMALモード(要約表示)=====================================");
        resp.getWriter().println(disp_SurfaceForm(tokensNormal));
        resp.getWriter().println("SEARCHモード(要約表示)=====================================");
        resp.getWriter().println(disp_SurfaceForm(tokensSearch));
        resp.getWriter().println("Extendsモード(要約表示)====================================");
        resp.getWriter().println(disp_SurfaceForm(tokensExtended));

        // 解析結果の表示(全パラメータ)
        resp.getWriter().println("NORMALモード(全パラメータ表示)================================");
        resp.getWriter().println(disp_all(tokensNormal));

    }

    public static String disp_SurfaceForm(List<Token> tokens) {
        String words="";

        for (Token token : tokens){
            words = words +
                    token.getSurfaceForm() + " | "; 
        }

        words = words + "\n";
        return words;
    }

    public static String disp_all(List<Token> tokens) {
        String words="";

        for (Token token : tokens){
            words = words +
                    "--------------------------------------------------" + "\n" + 
                    "allFeatures : " + token.getAllFeatures() + "\n" + 
                    "partOfSpeech : " + token.getPartOfSpeech() + "\n" + 
                    "position : " + token.getPosition() + "\n" + 
                    "reading : " + token.getReading() + "\n" + 
                    "SurfaceForm : " + token.getSurfaceForm() + "\n" + 
                    "allFeaturesArray : " + Arrays.asList(token.getAllFeaturesArray()) + "\n" + 
                    "辞書にある言葉? : " + token.isKnown() + "\n" + 
                    "未知語? : " + token.isUnknown() + "\n" + 
                    "ユーザ定義? : " + token.isUser() + "\n"; 
        }

        return words;
    }

}

次に、動かしてみた結果を載せておきます。

「日本経済新聞でモバゲーの記事を読んだ。2013年のLucene/Solrは検索エンジンです。」という文章を、形態素解析させてみました。以下のような感じでgetリクエストを送った後に帰ってきた結果になります。

http://URL/?q=日本経済新聞でモバゲーの記事を読んだ。2013年のLucene/Solrは検索エンジンです。

量が多いので、途中を省略してあります。

TestKuromoji テスト
content= 日本経済新聞でモバゲーの記事を読んだ。2013年のLucene/Solrは検索エンジンです。
NORMALモード(要約表示)======================================
日本経済新聞 | で | モバゲー | の | 記事 | を | 読ん | だ | 。 | 2013 | 年 | の | Lucene | / | Solr | は | 検索 | エンジン | です | 。 | 

SEARCHモード(要約表示)======================================
日本 | 経済 | 新聞 | で | モバゲー | の | 記事 | を | 読ん | だ | 。 | 2013 | 年 | の | Lucene | / | Solr | は | 検索 | エンジン | です | 。 | 

Extendsモード(要約表示)======================================
日本 | 経済 | 新聞 | で |  |  |  | ー | の | 記事 | を | 読ん | だ | 。 | 2 | 0 | 1 | 3 | 年 | の | L | u | c | e | n | e | / | S | o | l | r | は | 検索 | エ | ン | シ | ゙ | ン | です | 。 | 

NORMALモード(全パラメータ表示)======================================
--------------------------------------------------
allFeatures : 名詞,固有名詞,組織,*,*,*,日本経済新聞,ニホンケイザイシンブン,ニホンケイザイシンブン
partOfSpeech : 名詞,固有名詞,組織,*
position : 0
reading : ニホンケイザイシンブン
SurfaceForm : 日本経済新聞
allFeaturesArray : [名詞, 固有名詞, 組織, *, *, *, 日本経済新聞, ニホンケイザイシンブン, ニホンケイザイシンブン]
辞書にある言葉? : true
未知語? : false
ユーザ定義? : false
--------------------------------------------------
allFeatures : 助詞,格助詞,一般,*,*,*,で,デ,デ
partOfSpeech : 助詞,格助詞,一般,*
position : 6
reading : デ
SurfaceForm : で
allFeaturesArray : [助詞, 格助詞, 一般, *, *, *, で, デ, デ]
辞書にある言葉? : true
未知語? : false
ユーザ定義? : false
--------------------------------------------------
 :  :
 (省略)

(GAEで日本語の形態素解析を試してみる)第2回 lucene-gosenのTokenFilterを試す

GoogleAppEngineの勉強中です。
3回に分けて、GAEで日本語の形態素解析を試してみたことを書いています。
第1回 lucene-gosenを使ってみる
第2回 lucene-gosenのTokenFilterを試す
第3回 Kuromojiを使ってみる

今回は、第2回です。

使っている環境
Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1

Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

lucene-gosen-4.2.0-naist-chasen
lucene-core-4.2.0


(1)はじめに
第1回で、lucene-gosenを使ってみました。
その後、lucene-gosenには、TokenFilter という機能があって、7種類のフィルタが使えるという情報を見つけました。
(参考) moco(beta)’s backup: lucene-gosen の Tokenizer, TokenFilter を動かしてみる

その中の、GosenWidthFilter というものが気になったので、今回、試してみました。
GosenWidthFilter というのは、

Folds fullwidth ASCII variants into the equivalent basic latin
 → 全角のASCII文字を半角にする
Folds halfwidth Katakana variants into the equivalent kana
 → 半角カタカナを全角にする

というものです。
(参考)GosenWidthFilter (lucene-gosen-2.0.2 API)

たとえば、「ニュースサイトが提供している、TwitterやRSSフィードの内容を定期的に取得して、そこに含まれる単語を集計してみたり」ってことをやるときに、同じ単語でも半角・全角がまざっているとうまく集計ができない。
自分でうまく変換すればいいのかもしれないけど、形態素解析ライブラリのほうで、うまいことやってくれるのなら助かります。

というわけで、準備に取り掛かります。


(2)lucene-gosenダウンロード
こちらについては、第1回めに記載してあるので、そちらを参照してください。


(3)lucene-coreダウンロード
GosenWidthFilter機能は、lucene-gosenの機能なので、lucene本体はいらないと思っていたんだけど、実際にGosenWidthFilterをつかおうとすると、lucene本体に含まれるクラスが必要でした。

こちらのサイトから、
Apache Lucene – Welcome to Apache Lucene

Apache LuceneのDOWNLOADをクリック。
ダウンロードしたlucene-4.2.0.tgz というファイルを展開すると、coreというフォルダの中に、lucene-core-4.2.0.jar というJarファイルがあります。

こちらを、クラスパスに通しておけば準備完了。


(4)コーディングの流れ

第1回にもかいたんだけど、まずはじめに、注意点です。
僕はGoogleAppEngine上で動かしています。
で、今回テスト用に作成するものは、HTTPのGETリクエストに「q=解析したい文章」といったパラメータを付けることで、パラメータqに設定した文章を解析して、ブラウザに結果を返すように作っています。

そのため、以下のようにして、getリクエストのパラメータqを取得して、変数contentに格納するようにしています。

String content = req.getParameter("q");

また、結果をブラウザに返却するため、以下のメソッドで出力しています。

resp.getWriter().println("出力内容をここに書く");

このあたりはGAEならではの作法になります。

それを踏まえて、GosenWidthFilter機能の使い方をみていきます。

基本的には、先述したサイトに載っているコードを、ほぼそのまま使っています。
(参考) moco(beta)’s backup: lucene-gosen の Tokenizer, TokenFilter を動かしてみる

やり方は、解析したい文章をGosenTokenizerに掛けて、そのあとGosenWidthFilterでフィルタリング(?)するだけです。

        TokenStream stream = new GosenTokenizer(new StringReader(content));
        stream = new GosenWidthFilter(stream);

そうするとstreamの中に、形態素解析した結果が入ります。

解析結果は以下のような感じで、getAttributeメソッドでとりだせます。

while(stream.incrementToken()) {
    CharTermAttribute charAtt = stream.getAttribute(CharTermAttribute.class);
    BasicFormAttribute bfAtt = stream.getAttribute(BasicFormAttribute.class);
    ConjugationAttribute conjAtt = stream.getAttribute(ConjugationAttribute.class);
       :       :       :
        (中略)

    resp.getWriter().println(
        "======================================\n" +
        "CharTerm=" + charAtt + "\t" +  // トークン
        "BasicForm=" + bfAtt.getBasicForm() + "\t" +  // 基本形
        "ConjugationalForm=" + conjAtt.getConjugationalForm() + "\t" +  // 活用形
       :       :       :
        (中略)

        );
}

取得できる情報(Attribute)にどんなものがあるかについては、こちらのドキュメントが参考になります。
Attribute (Lucene 4.2.0 API)

さて、基本的な流れはこれでいいんですが、実際に動かしてみると、「辞書に載っていない英単語」や「数値」が、一文字ずつに分割されてしまいます。

たとえば以下の文章を形態素解析してみると、
「2013年のLucene/Solrは検索エンジンです。」
2013が、「2,0,1,3」の数字4つに分割され、Luceneは「L,u,c,e,n,e」というアルファベット6文字に分割されてしまいます。

第1回目には、これに対処するために、compositePOSという機能を使いました。「トークン」を「合成」するための機能らしいです。

今回もこの機能を使おうとしたんだけども、力及ばず、わからなかった。。。

第1回のときは、StringTaggerクラスを使って解析を行っていました。で、StringTaggerクラスのaddFilterメソッドで、CompositeTokenFilterを追加していました。
今回の場合、GosenTokenizerを使っているので、GosenTokenizerを動かす時にフィルタを適用すればいいと思って試してみたんだけど、ダメだった。

// String rule = "記号-アルファベット";
// CompositeTokenFilter ctFilter = new CompositeTokenFilter();
// ctFilter.readRules(new BufferedReader(new StringReader(rule)));
// TokenStream stream = new GosenTokenizer(new StringReader(content), ctFilter);

というわけで、若干、消化不良になってしまった。


(5)ソースコード
実際に作成したサンプルアプリのソースコードと、動かした出力結果を書いておきます。

まずは、ソースコードから。

package sample;

import java.io.*;
import java.util.List;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.lucene.analysis.gosen.GosenWidthFilter;
import org.apache.lucene.analysis.gosen.GosenTokenizer;
import org.apache.lucene.analysis.gosen.tokenAttributes.*;

import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.*;

@SuppressWarnings("serial")
public class TestGosenWidthFilter_Servlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {

        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/plain; charset=UTF-8");
        resp.getWriter().println("GosenWidthFilter テスト");

        // パラメータ取得
        String content = req.getParameter("q");
        if (content == null || content.length() == 0){
            content = "";
        }

        resp.getWriter().printf("content= %s%n", content);

        // String rule = "記号-アルファベット";
        // CompositeTokenFilter ctFilter = new CompositeTokenFilter();
        // ctFilter.readRules(new BufferedReader(new StringReader(rule)));
        // TokenStream stream = new GosenTokenizer(new StringReader(content), ctFilter);
        TokenStream stream = new GosenTokenizer(new StringReader(content));

        stream = new GosenWidthFilter(stream);

        try {

            while(stream.incrementToken()) {
                CharTermAttribute charAtt = stream.getAttribute(CharTermAttribute.class);
                BasicFormAttribute bfAtt = stream.getAttribute(BasicFormAttribute.class);
                ConjugationAttribute conjAtt = stream.getAttribute(ConjugationAttribute.class);
                PartOfSpeechAttribute posAtt = stream.getAttribute(PartOfSpeechAttribute.class);
                PronunciationsAttribute pronAtt = stream.getAttribute(PronunciationsAttribute.class);
                List pronunciations = pronAtt.getPronunciations();
                ReadingsAttribute readAtt = stream.getAttribute(ReadingsAttribute.class);
                List readings = readAtt.getReadings();

                SentenceStartAttribute senAtt = stream.getAttribute(SentenceStartAttribute.class);
                CostAttribute costAtt = stream.getAttribute(CostAttribute.class);
                OffsetAttribute offsetAtt = stream.getAttribute(OffsetAttribute.class);
                TermToBytesRefAttribute term_bytesAtt = stream.getAttribute(TermToBytesRefAttribute.class) ;

                resp.getWriter().println(
                    "======================================\n" +
                    "CharTerm=" + charAtt + "\t" +  // トークン
                    "OffsetAttribute=" + offsetAtt.startOffset() + "\t" +
                    "CharTerm.length=" + charAtt.length() + "\t" +  // トークン
                    "CostAttribute=" + costAtt.getCost() + "\n" +

                    "BasicForm=" + bfAtt.getBasicForm() + "\t" +  // 基本形
                    "ConjugationalForm=" + conjAtt.getConjugationalForm() + "\t" +  // 活用形
                    "ConjugationalType=" + conjAtt.getConjugationalType() + "\n" +  // 活用形

                    "PartOfSpeechAttribute=" + posAtt.getPartOfSpeech() + "\t" + // 品詞
                    "PronunciationsAttribute=" + (pronunciations.size() > 0 ? pronAtt.getPronunciations().get(0) : "") + "\t" +  // 発音
                    "ReadingsAttribute=" + (readings.size() > 0 ? readAtt.getReadings().get(0) : "") + "\n" +  // 読み

                    "term_bytesAtt=" + term_bytesAtt+ "\n"

                    );
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

次に、動かしてみた結果を載せておきます。
「2013年のLucene/Solrは検索エンジンです。」という文章を、形態素解析させてみました。以下のような感じでgetリクエストを送った後に帰ってきた結果になります。

http://URL/?q=2013年のLucene/Solrは検索エンジンです。

量が多いので、途中を省略してあります。
全角アルファベット「Solr」が半角に変換され、間隔カタカナ「エンジン」が全角「エンジン」に変換されました!

GosenWidthFilter テスト
content= 2013年のLucene/Solrは検索エンジンです。
======================================
CharTerm=2	OffsetAttribute=0	CharTerm.length=1	CostAttribute=4688
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=名詞-数	PronunciationsAttribute=ニ	ReadingsAttribute=ニ
term_bytesAtt=2

======================================
CharTerm=0	OffsetAttribute=1	CharTerm.length=1	CostAttribute=3759
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=名詞-数	PronunciationsAttribute=ゼロ	ReadingsAttribute=ゼロ
term_bytesAtt=0

     :     :
     (中略)

======================================
CharTerm=年	OffsetAttribute=4	CharTerm.length=1	CostAttribute=1053
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=名詞-接尾-助数詞	PronunciationsAttribute=ネン	ReadingsAttribute=ネン
term_bytesAtt=年

======================================
CharTerm=の	OffsetAttribute=5	CharTerm.length=1	CostAttribute=721
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=助詞-連体化	PronunciationsAttribute=ノ	ReadingsAttribute=ノ
term_bytesAtt=の

======================================
CharTerm=L	OffsetAttribute=6	CharTerm.length=1	CostAttribute=4543
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-アルファベット	PronunciationsAttribute=エル	ReadingsAttribute=エル
term_bytesAtt=L

======================================
CharTerm=u	OffsetAttribute=7	CharTerm.length=1	CostAttribute=3248
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-アルファベット	PronunciationsAttribute=ユー	ReadingsAttribute=ユー
term_bytesAtt=u

     :     :
     (中略)

======================================
CharTerm=/	OffsetAttribute=12	CharTerm.length=1	CostAttribute=3896
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-一般	PronunciationsAttribute=/	ReadingsAttribute=/
term_bytesAtt=/

======================================
CharTerm=S	OffsetAttribute=13	CharTerm.length=1	CostAttribute=2414
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-アルファベット	PronunciationsAttribute=エス	ReadingsAttribute=エス
term_bytesAtt=S

======================================
CharTerm=o	OffsetAttribute=14	CharTerm.length=1	CostAttribute=3248
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-アルファベット	PronunciationsAttribute=オー	ReadingsAttribute=オー
term_bytesAtt=o

     :     :
     (中略)

======================================
CharTerm=検索	OffsetAttribute=18	CharTerm.length=2	CostAttribute=3599
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=名詞-サ変接続	PronunciationsAttribute=ケンサク	ReadingsAttribute=ケンサク
term_bytesAtt=検索

======================================
CharTerm=エンジン	OffsetAttribute=20	CharTerm.length=4	CostAttribute=30883
BasicForm=*	ConjugationalForm=null	ConjugationalType=null
PartOfSpeechAttribute=未知語	PronunciationsAttribute=	ReadingsAttribute=
term_bytesAtt=エンジン

======================================
CharTerm=です	OffsetAttribute=25	CharTerm.length=2	CostAttribute=2328
BasicForm=*	ConjugationalForm=基本形	ConjugationalType=特殊・デス
PartOfSpeechAttribute=助動詞	PronunciationsAttribute=デス	ReadingsAttribute=デス
term_bytesAtt=です

======================================
CharTerm=。	OffsetAttribute=27	CharTerm.length=1	CostAttribute=361
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-句点	PronunciationsAttribute=。	ReadingsAttribute=。
term_bytesAtt=。

(GAEで日本語の形態素解析を試してみる)第1回 lucene-gosenを使ってみる

GoogleAppEngineの勉強中です。
ニュースサイトが提供している、TwitterやRSSフィードの内容を定期的に取得して、そこに含まれる単語を集計してみたり、ってことをやってみたいと思った。とりあえず、IT系の情報を収集してみて、自分用の「今週のトレンド情報」を作ってみるとか。

で、そうすると必然的に、日本語の文章から単語を抽出する必要がある。
Pythonで使える「日本語の形態素解析ライブラリ」ないかなー、って探してみたけど、イマイチ。
というわけで、Javaでやることにした。

いくつか試してみたので、以下の3回に分けて書きます。
第1回 lucene-gosenを使ってみる
第2回 lucene-gosenのTokenFilterを試す
第3回 Kuromojiを使ってみる

今回は、第1回です。

使っている環境
Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1

Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

lucene-gosen-4.2.0-naist-chasen


(1)lucene-gosenダウンロード

こちらのサイトの、Downloadsから、
lucene-gosen – Japanese analysis for Apache Lucene/Solr 3.6 and 4.2 – Google Project Hosting

辞書を内包しているJarファイルをダウンロードします。
辞書の違い(naist-chasen、ipadic)があるみたいですが、どちらでもいいみたいです。
ただ、なんとなくnaist-chasenのほうがよさそうな気がしたので、僕はlucene-gosen-4.2.0-naist-chasen.jarにしました。

で、これをクラスパスに通しておけば、準備完了。


(2)コーディングの流れ

まずはじめに、注意点です。
僕はGoogleAppEngine上で動かしています。
で、今回テスト用に作成するものは、HTTPのGETリクエストに「q=解析したい文章」といったパラメータを付けることで、パラメータqに設定した文章を解析して、ブラウザに結果を返すように作っています。

そのため、以下のようにして、getリクエストのパラメータqを取得して、変数contentに格納するようにしています。

String content = req.getParameter("q");

また、結果をブラウザに返却するため、以下のメソッドで出力しています。

resp.getWriter().println("出力内容をここに書く");

このあたりはGAEならではの作法になります。

それを踏まえて、lucene-gosenの使い方をみていきます。
以下のように、taggerを用意してから、tagger.analyzeを実行すると、解析結果がlistの中に入ります。
(content には、解析対象の文章が入っています)

        StringTagger tagger = SenFactory.getStringTagger(null);
        List<Token> list = new ArrayList<Token>();
        list = tagger.analyze(content, list);

List<Token>の中身は、Tokenクラスのメソッドを使って取り出せます。たとえば、token.getSurface()と書くと、解析された単語が表示されます。
(参考)Token (lucene-gosen-2.0.2 API)

Tokenの中に、morpheme dataというものがあって、token.getMorpheme()を実行すると、Morphemeオブジェクトが返却されます。で、morpheme dataの内容は、Morphemeクラスのメソッドを使って取得することになります。

以下のような感じで書くことで、品詞(PartOfSpeech)を取得できます。
Morpheme morpheme = token.getMorpheme();
morpheme.getPartOfSpeech()
(参考)Morpheme (lucene-gosen-2.0.2 API)

まとめると、listの中身は、以下のような感じで書くと取り出せます。

            for (Token token : list) {

                resp.getWriter().println(
                        "======================================\n" +
                        "surface=" + token.getSurface() + "\t" +
                        "start=" + token.getStart() + "\t" +
                        "length=" + token.getLength() + "\t" +
                        "cost=" + token.getCost()
                        );

                Morpheme morpheme = token.getMorpheme();

                resp.getWriter().println(
                        "basicForm=" + morpheme.getBasicForm() + "\t" +
                        "cForm=" + morpheme.getConjugationalForm() + "\t" +
                        "cType=" + morpheme.getConjugationalType() + "\n" +
                        "partOfSpeech=" + morpheme.getPartOfSpeech() + "\t" +
                        "pron=" + morpheme.getPronunciations() + "\t" +
                        "read=" + morpheme.getReadings() + "\n" +
                        "additionalInfo=" + morpheme.getAdditionalInformation()
                    );
            }

ここまでのlucene-gosenの使い方は、こちらのサイトを参考にしています。
(参考)lucene-gosenで形態素解析 – Akira Koyasu’s WebLog

基本的な流れはこれでいいんですが、実際に動かしてみると、「辞書に載っていない英単語」や「数値」が、一文字ずつに分割されてしまいます。

たとえば以下の文章を形態素解析してみると、
「2013年のLucene/Solrは検索エンジンです。」
2013が、「2,0,1,3」の数字4つに分割され、Luceneは「L,u,c,e,n,e」というアルファベット6文字に分割されてしまいます。

これに対処するために、compositePOSという機能を使います。「トークン」を「合成」するための機能らしいです。

(参考)
compositePOSの利用例(naist-chasenでの英単語の出力方法例) | @johtani の日記
Solr/Tokenizer評価201105/JapaneseTokenizer – 春山征吾のWiki – livedoor Wiki(ウィキ)
Sprout #7 lucene-gosen | Apribase

トークンを合成する規則を設定することで、条件に一致したものをひとまとめにしてくれます。
書き方は「品詞名を半角スペース区切りで記述して、規則を設定する」とのことです。書き方はこうなっている。

連結品詞名 構成品詞名1 構成品詞名2 ... 構成品詞名n
 ・連結品詞名:合成したあとのトークンの品詞として出力する品詞名
 ・構成品詞名:合成したい品詞名(スペース区切りで複数指定可能)

今回は、「名詞-数」と、「記号-アルファベット」の規則を設定することにしました。

コーディングのときは、以下のような書き方になる。

        CompositeTokenFilter ctFilter = new CompositeTokenFilter();

        ctFilter.readRules(new BufferedReader(new StringReader("名詞-数")));
        tagger.addFilter(ctFilter);

        ctFilter.readRules(new BufferedReader(new StringReader("記号-アルファベット")));
        tagger.addFilter(ctFilter);

このcompositePOS設定を追加した後に、改めて以下の文章を形態素解析すると、
「2013年のLucene/Solrは検索エンジンです。」

こんな感じに分割されます。
「2013,年,の,Lucene,/,Solr,は,検索,エンジン,です,。」


(3)ソースコード

実際に作成したサンプルアプリのソースコードと、動かした出力結果を書いておきます。
まずは、ソースコードから。

package sample;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.java.sen.SenFactory;
import net.java.sen.StringTagger;
import net.java.sen.dictionary.Morpheme;
import net.java.sen.dictionary.Token;
import net.java.sen.filter.stream.CompositeTokenFilter;

@SuppressWarnings("serial")
public class TestGosen_Servlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {

        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/plain; charset=UTF-8");
        resp.getWriter().println("Gosen テスト");

        // パラメータ取得
        String content = req.getParameter("q");
        if (content == null || content.length() == 0){
            content = "";
        }

        resp.getWriter().printf("content= %s%n", content);

        // Gosen
        StringTagger tagger = SenFactory.getStringTagger(null);

        // compositePOSを設定
        CompositeTokenFilter ctFilter = new CompositeTokenFilter();

        ctFilter.readRules(new BufferedReader(new StringReader("名詞-数")));
        tagger.addFilter(ctFilter);

        ctFilter.readRules(new BufferedReader(new StringReader("記号-アルファベット")));
        tagger.addFilter(ctFilter);

        // 解析実行
        try {
            List<Token> list = new ArrayList<Token>();
            list = tagger.analyze(content, list);

            for (Token token : list) {

                resp.getWriter().println(
                        "======================================\n" +
                        "surface=" + token.getSurface() + "\t" +
                        "start=" + token.getStart() + "\t" +
                        "length=" + token.getLength() + "\t" +
                        "cost=" + token.getCost()
                        );

                Morpheme morpheme = token.getMorpheme();

                resp.getWriter().println(
                        "basicForm=" + morpheme.getBasicForm() + "\t" +
                        "cForm=" + morpheme.getConjugationalForm() + "\t" +
                        "cType=" + morpheme.getConjugationalType() + "\n" +
                        "partOfSpeech=" + morpheme.getPartOfSpeech() + "\t" +
                        "pron=" + morpheme.getPronunciations() + "\t" +
                        "read=" + morpheme.getReadings() + "\n" +
                        "additionalInfo=" + morpheme.getAdditionalInformation()
                    );
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

次に、動かしてみた結果を載せておきます。
「2013年のLucene/Solrは検索エンジンです。」という文章を、形態素解析させてみました。以下のような感じでgetリクエストを送った後に帰ってきた結果になります。

http://URL/?q=2013年のLucene/Solrは検索エンジンです。

Gosen テスト
content= 2013年のLucene/Solrは検索エンジンです。
======================================
surface=2013	start=0	length=4	cost=41969
basicForm=*	cForm=*	cType=*
partOfSpeech=名詞-数	pron=[ニゼロイチサン]	read=[ニゼロイチサン]
additionalInfo=null
======================================
surface=start=4	length=1	cost=17460
basicForm=*	cForm=*	cType=*
partOfSpeech=名詞-接尾-助数詞	pron=[ネン]	read=[ネン]
additionalInfo=null
======================================
surface=start=5	length=1	cost=18181
basicForm=*	cForm=*	cType=*
partOfSpeech=助詞-連体化	pron=[ノ]	read=[ノ]
additionalInfo=null
======================================
surface=Lucene	start=6	length=6	cost=185064
basicForm=*	cForm=*	cType=*
partOfSpeech=記号-アルファベット	pron=[エルユーシーイーエヌイー]	read=[エルユーシーイーエヌイー]
additionalInfo=null
======================================
surface=/	start=12	length=1	cost=42860
basicForm=*	cForm=*	cType=*
partOfSpeech=記号-一般	pron=[/]	read=[/]
additionalInfo=null
======================================
surface=Solr	start=13	length=4	cost=200584
basicForm=*	cForm=*	cType=*
partOfSpeech=記号-アルファベット	pron=[エスオーエルアール]	read=[エスオーエルアール]
additionalInfo=null
======================================
surface=	start=17	length=1	cost=56498
basicForm=*	cForm=*	cType=*
partOfSpeech=助詞-係助詞	pron=[ワ]	read=[ハ]
additionalInfo=null
======================================
surface=検索	start=18	length=2	cost=60097
basicForm=*	cForm=*	cType=*
partOfSpeech=名詞-サ変接続	pron=[ケンサク]	read=[ケンサク]
additionalInfo=null
======================================
surface=エンジン	start=20	length=5	cost=90980
basicForm=*	cForm=null	cType=null
partOfSpeech=未知語	pron=[]	read=[]
additionalInfo=null
======================================
surface=です	start=25	length=2	cost=93308
basicForm=*	cForm=基本形	cType=特殊・デス
partOfSpeech=助動詞	pron=[デス]	read=[デス]
additionalInfo=null
======================================
surface=	start=27	length=1	cost=93669
basicForm=*	cForm=*	cType=*
partOfSpeech=記号-句点	pron=[。]	read=[。]
additionalInfo=null