Jersey MVC + Thymeleafしたら文字化けた時の対策

環境

  • OS X 10.11.4
  • Google Chrome
  • Payara Web ML 4.1.1.161.1 (Jersey 2.22.1)
  • Thymeleaf 2.1.4.RELEASE

プログラム概要

@bufferingさんの記事を参考に、Thymeleaf用のTemplateProcessorを作成。

@Provider
public class ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {

    @Inject
    private javax.inject.Provider<Ref<HttpServletRequest>> requestProviderRef;
    @Inject
    private javax.inject.Provider<Ref<HttpServletResponse>> responseProviderRef;

    private TemplateEngine templateEngine;

    @Inject
    public ThymeleafTemplateProcessor(Configuration config, ServletContext servletContext) {
        super(config, servletContext, "html", "html");
        TemplateResolver templateResolver = new ServletContextTemplateResolver();
        templateResolver.setPrefix((String)config.getProperty(MvcFeature.TEMPLATE_BASE_PATH));
        templateResolver.setSuffix(".html");
        templateEngine = new TemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
    }

    @Override
    protected String resolve(String templatePath, Reader reader) throws Exception {
        return templatePath;
    }

    @Override
    public void writeTo(String templateReference, Viewable viewable, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws IOException {
        HttpServletRequest httpServletRequest = requestProviderRef.get().get();
        HttpServletResponse httpServletResponse = responseProviderRef.get().get();
        WebContext webContext = new WebContext(
                httpServletRequest, httpServletResponse, 
                super.getServletContext(), httpServletRequest.getLocale());
        Object model = viewable.getModel();
        if (model instanceof Map) {
            Map<String, Object> map = (Map) model;
            webContext.setVariables(map);
        } else {
            Map<String, Object> variables = new HashMap<>();
            variables.put("model", viewable.getModel());
            webContext.setVariables(variables);
        }
        templateEngine.process(viewable.getTemplateName(), webContext, 
                httpServletResponse.getWriter());
    }

}

リソースクラス。

@Path("employee")
@RequestScoped
@Produces(MediaType.TEXT_HTML)
public class EmployeeResource {
    
    @GET
    @Path("index")
    public Viewable index() throws Exception {
        return new Viewable("/employee/index");
    }
}

Thymeleafのビュー。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>入力画面</title>
    <link rel="stylesheet" href="../../css/style.css"/>
</head>
<body>
<p>社員IDを入力してください(77, 88, 99で例外発生)</p>

<!-- 検証エラーメッセージの表示 -->
<ul class="error" th:if="${violations != null}">
    <li th:each="violation : ${violations}" th:text="${violation.message}">ダミーのメッセージ</li>
</ul>

<form action="./result" method="get">
    社員ID:<input type="text" name="id" value="" th:value="${param.id == null} ? '' : ${param.id[0]}"/>
    <input type="submit" value="検索"/>
</form>
</body>
</html>

現象

画面の全日本語メッセージが文字化けている。

f:id:MasatoshiTada:20160501145927p:plain

原因と対策

レスポンスヘッダーやブラウザが現在開いている文字コードを見ると、正しくUTF-8になっていた。

よって、そもそもレスポンスを書き込む際に文字コードが正しく設定されていないっぽい。

TemplateProcessorにhttpServletResponse.setCharacterEncoding("UTF-8");を追加したら、正しく表示された。

f:id:MasatoshiTada:20160501150201p:plain

@Provider
public class ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {

    // 中略
    
    @Override
    public void writeTo(String templateReference, Viewable viewable, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws IOException {
        HttpServletRequest httpServletRequest = requestProviderRef.get().get();
        HttpServletResponse httpServletResponse = responseProviderRef.get().get();

        // これを追加
        httpServletResponse.setCharacterEncoding("UTF-8");

        WebContext webContext = new WebContext(
                httpServletRequest, httpServletResponse, 
                super.getServletContext(), httpServletRequest.getLocale());
        Object model = viewable.getModel();
        if (model instanceof Map) {
            Map<String, Object> map = (Map) model;
            webContext.setVariables(map);
        } else {
            Map<String, Object> variables = new HashMap<>();
            variables.put("model", viewable.getModel());
            webContext.setVariables(variables);
        }
        templateEngine.process(viewable.getTemplateName(), webContext, 
                httpServletResponse.getWriter());
    }

}

ちなみに

Ozark + Thymeleafだと、この記述が無くても正しく表示された。

(ViewEngineクラスにもそれらしい記述は無い)

理由は不明。。。

Cloud Foundryワークショップに参加してきました! #cfws

2016-04-05(火)に開催された、Pivotalさん主催のCloud Foundryワークショップに参加してきました!

クラウド関連のことはあまり知識がないので、クラウドの世界ってどんなものなんだろう?ということ知りたかったというのが動機です。

講師は@makingさん。冒頭は、3月のJJUGナイトセミナーの資料でCloud Foundryの概要説明でした。

理解できた範囲で要約すると、

  • IaaS上にPaaSを構築・管理するためのソフトウェア群
  • Pivotal社単独ではなく、「Cloud Foundry Foundation」という団体が仕様を策定している
  • Pivotal Web Servicesは、AWS上にCloud FoundryでPaaSを構築済みのもの。アカウントを作成すればすぐに利用できる
  • PCF Devは、ローカルPC上にCloud Foundry環境を構築するもの
  • Cloud Foundry上へのアプリケーションのデプロイなどは、CLIツールをインストールしてcfコマンドで行う

その他、Cloud Foundry内部の仕組みや、Blue-Green Deployなどの説明もありました。 (なんか理解が間違ってたらツッコミください・・・)

その後、下記の資料でハンズオン。

GitHub - Pivotal-Japan/cf-workshop: Cloud Foundry Workshop

Cloud Foundryワークショップ資料 - Qiita

(上記のGitHubとQiitaは、両方同じ資料です)

特に印象深かったのは、「5. スケールアウト」と「7. Blue-Greenデプロイ」です。

例えば、より多くのクライアントに対応するためにインスタンス数を増やして負荷分散したいという場合は、ローカルPCから下記のコマンドを叩くだけ。

$ cf scale -i 4 hello-redis-tada

上記だとインスタンス数が4になります。かかった時間も、ものの数秒でした。で、ロードバランシングとかはCloud Foundry内のRouterがよろしくやってくれる、と。

Blue-Greenデプロイは、名前だけは聞いたことがあったものの、どんなものかはよく知りませんでした。

新しいバージョンのアプリケーションをデプロイする際に、ダウン時間をなるべくゼロに近づけるための手法だと理解しました。

これも、Cloud Foundryを使えば非常に簡単ですね。

クラウドって実際に触ったことがない」という人にはとてもオススメのワークショップです!

これから何回か開催予定ということなので、是非参加してみてはいかがでしょうか。

ハンズオンはボリュームがそこそこありますので、当日は余裕を持って話を聞きたいという方は「3. 簡単なアプリケーションをデプロイ」くらいまでは事前に予習しておくといいかもしれません。

TOEIC受験記(2016年3月)

なぜ受験しようと思ったか

僕の仕事は研修講師(主にプログラミング)なので、色々と調べ物をする時にどうしても英語の仕様書・書籍・ブログ記事などを読む必要が出てきます。やはり、ITの本場はアメリカです。

なので、もっとリーディング力を高めたいと思ったのがきっかけです。

勉強前の英語レベルは?

中学までは英語は得意科目でしたが、高校になるとマークシート形式ならなんとか、という感じでした(センター試験だと8〜9割くらい)。でも、記述式はまったくダメでした。

10年くらい前、まだ大学生の頃、試しにTOEICを受けたことがありましたが、たしか440〜450点だったと思います。

結果は?

Listening Reading Total
395 430 825

勉強法は?

僕の勉強法はいたってシンプルで、市販書籍でインプットとアウトプットをひたすら繰り返すのみです。

勉強時間は、主に通勤時間と土日です。家族に感謝。

改訂版 TOEIC(R) TEST 英文法 出るとこだけ!

Amazon.co.jp | 音声DL付 改訂版 TOEIC(R) TEST 英文法 出るとこだけ! | 本 ・TOEIC 通販

まずはじめに読んだ本です。文法のインプット・アウトプットに使いました。ページは少ないけど文法を理解するコツがしっかりと解説されていて、記憶力も根気も無い自分にはピッタリの本でした。あえて受験テクニック的な見出しが付いていますが、文法的な背景も解説されています。

僕は暗記物がとても苦手なので、いきなり単語集をやると確実に挫折します。というか、実際に挫折しました・・・。なので、文法で理論的な部分をある程度身につけてから、単語に取り掛かりました。その方がモチベーションも続きます。

この本は3回くらい繰り返しました。

改訂版 TOEIC(R)TEST 英単語 出るとこだけ!

Amazon.co.jp | CD-ROM付 改訂版 TOEIC(R)TEST 英単語 出るとこだけ! | 本 ・TOEIC 通販

文法の勉強をしたはいいが、やはり単語が分からないことにはリーディングができないことに気付き、文法の本と同じ小石裕子さんの本を読みました。

TOEICは出る単語が(文法もですが)決まっている感じがするので、TOEICを受けるならTOEIC専用の単語集を使ったほうがいいと思います。

この本は、5回くらい繰り返しました。

TOEICテスト公式プラクティス リーディング編

Amazon.co.jp | TOEICテスト公式プラクティス リーディング編 | 本 ・TOEIC 通販

TOEICを実施しているETSが出している、公式の練習問題集です。これは2回くらい繰り返しました。

改訂版 TOEIC(R) TEST リスニング 出るとこだけ!

Amazon.co.jp | CD付 改訂版 TOEIC(R) TEST リスニング 出るとこだけ! | 本 ・TOEIC 通販

今回はリーディングに絞って勉強するつもりだったのですが、時間に少し余裕が出来たので、直前の2週間だけリスニングの勉強をしました。

読んだのは、やはり小石裕子さんの本です。これは1回通りやるのが精一杯でした。

TOEICテスト新公式問題集< Vol.6>

Amazon.co.jp | TOEICテスト新公式問題集< Vol.6> | 本 ・TOEIC 通販

本番の試験と同じ形式の模試が、2回分入っています。1回目は普通に問題集として解き、2回目を実際の時間(2時間)を測って解きました。

下記サイトによると、Vol.6が最も本番の難易度に近いそうです。 僕はこのVol.6しか解いていませんが、確かに本番の試験と難易度は似ていると感じました。

TOEIC 公式問題集はどれから解けば良いの?

勉強してどうだったか

英語の文書を読むのが少し楽になったように感じます。

これは、勉強したことにより地力が鍛えられたこともありますが、スコアを取ったことによる自信も大きいと思います。

勉強する→結果が出る→自信がつく→モチベーションが湧く→勉強する→・・・

このサイクルを回し続けることが肝心です。

今後、また受験するかは未定ですが、英語文書をより読んでいって更に英語力を鍛えていこうと考えています。

使ってないけどやっておいたほうが良かったかなと思う本

TOEICテスト公式プラクティス リスニング編

Amazon.co.jp | TOEICテスト公式プラクティス リスニング編 | 本 ・TOEIC 通販

公式プラクティスのリスニング版です。今回は時間の都合上やりませんでした。

TOEICテスト 公式問題で学ぶボキャブラリー

Amazon.co.jp | TOEICテスト 公式問題で学ぶボキャブラリー | 本 ・TOEIC 通販

これも時間の都合上やりませんでした。

TOEICテスト新公式問題集〈Vol.4〉

Amazon.co.jp | TOEICテスト新公式問題集〈Vol.4〉 | 本 ・TOEIC 通販

前述のサイトによると、公式問題集の中でも一番難しいものだそうです。ハイスコアを目指す人はやっておくといいかもしれません。

改訂版 TOEIC(R) TEST 文法・語彙出るとこだけ! 問題集

Amazon.co.jp | 音声DL付 改訂版 TOEIC(R) TEST 文法・語彙出るとこだけ! 問題集 | 本 ・TOEIC 通販

これも時間の都合上やりませんでした。

TOEICテスト中学英文法で600点!

Amazon.co.jp | 新TOEICテスト中学英文法で600点! | 本 ・TOEIC 通販

中学英語に自信が無い人は、これから始めるといいかもしれません。

Jersey MVCのレスポンスのContent-Typeが「*/*」になる問題対策

詳細は後ほど追記します。

ContianerResponseFilterだと何故かできなかった・・・。

Jersey MVCは、MessageBodyWriterの中でContent-Typeを上書きしています。

ContianerResponseFilterはMessageBodyWriterよりも前に実行されるので、そりゃあ効かないですね・・・。

なので、必ずWriterInterceptorの「後処理」として書く必要があります。

MessageBodyWriterでなぜ「*/*」に上書きされるのかは、現在ソースを読んで調査中です。

package com.example.rest.interceptor;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import java.io.IOException;

@Provider
public class ContentTypeWriterInterceptor implements WriterInterceptor {
    @Override
    public void aroundWriteTo(WriterInterceptorContext context) 
            throws IOException, WebApplicationException {
        // デバッグログ
        System.out.println("======== BEFORE : " + context.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
        // レスポンスへの書き込み実行(必須)
        context.proceed(); 
        // デバッグログ
        System.out.println("======== AFTER : " + context.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
        // レスポンスヘッダーに "Content-Type: text/html"を上書きする
        MultivaluedMap<String, Object> headers = context.getHeaders();
        headers.putSingle(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML);
    }
}

Payara 161.1 で @Transactional のバグが修正されました!

この記事は、半分ポエムです。

最初のきっかけ

下記の@opengl_8080さんの記事でした。

JavaEE使い方メモ(JTA) - Qiita

CDIビーンのメソッドに@Transactionalアノテーションを付加しても、非チェック例外発生時にロールバックされない、というバグがあるのこと。

JTAの仕様としては、チェック例外ならばコミット、非チェック例外ならばロールバックされます)

早速、自分でも試してみたのですが、やっぱりロールバックされませんでした。

はじめてのISSUE報告(2015/11/1)

僕は、トランザクション管理はJava EEの要だと思っていますので、PayaraのGitHubにISSUEを書きました。

英語あんまり得意じゃないですが・・・(^^;

テスト用プログラム(上記の@opengl_8080さんの記事を参考に作りました)

GitHub - MasatoshiTada/TransactionalSample-Doma

ISSUE

Payara does NOT rollback when RuntimeException occurs in CDI @Transactional method using JDBC · Issue #505 · payara/Payara · GitHub

いま読み返しても、ひどい英語だ・・・。でも、もうこれは気合いでした。

Payaraの中の人から反応が!(2015-11-15)

Payaraの中の人たちは、すぐに反応してくれました。

テスト用プログラムのおかげで再現性がすぐに認められ、バグ修正も速やかに行われました!

PAYARA-510 fixes 505 ensures resources are enlisted into the transaction by smillidge · Pull Request #524 · payara/Payara · GitHub

最初にバグを見つけたのは@opengl_8080さんですし、僕はISSUEを書いただけです。

しかし、僕は現役のエンジニアではなく、僕が作ったプログラムが世の中で動くわけではないので、

本当に、ほんのちょっとでもPayaraに貢献できたことが、本当に嬉しかったんです。

修正されたPayaraがリリース!しかし・・・(2016-01)

上記の修正は、Payara 4.1.1.161に含まれました。

ウキウキして早速試してみました。

すると・・・

直ってないやん(--;)

TxTypeREQUIREDの挙動は直っていたんですが、REQUIRES_NEWの挙動は直っていませんでした。

再びISSUE報告(2016-02)

数日後、改めてISSUEを書きました。

#505 is not fixed when TxType is REQUIRES_NEW · Issue #667 · payara/Payara · GitHub

すると、やはりすぐに反応が返ってきました。

前回のことを覚えてくれていたのか、すぐに修正も行われました。

ちなみに、2回とも修正対応してくれたsmillidgeさん、Payaraを開発している英国C2B2社の創業者の方だそうです。

もし今年のJava Day Tokyoとかにいらっしゃったりすれば、是非お話してみたいなあ・・・。

リリースが思わぬ速さで!(2016-03)

Payaraは通常、四半期に1回のリリースなんですが、たまに四半期の間でもパッチ版がリリースされます。

僕は、「修正版が出るのは次の四半期かなー」と思っていたんですが、つい先日、パッチ版のPayara 4.1.1.161.1がリリースされました!

(「1」が多いので、以降は「Payara 161.1」と書きます)

このリリースに、上記バグの修正が含まれています。

このバグ修正の何が嬉しいのか?

JPA以外のORマッパーをPayara上で使いつつ、かつ@Transactionalでトランザクション管理を行うことができます。

(上記バグは、以前からJPAでは発生しませんでした)

最近、「100%Java EE標準」にこだわらないのであれば、JPA以外のORマッパー(Doma・MyBatis・jOOQ・DBFluteなど)を使う選択肢もアリだと思っています。

このような構成でも、安心してPayaraを使うことができます。

最後に

このバグを見つけてくれた@opengl_8080さんには、本当に感謝です。

繰り返しますが、僕はISSUEを書いただけです。

でも、将来的には、自分でバグ修正までしてプルリクエストを送れるくらいになりたいなあ・・・。

Jersey Test + MockitoでJAX-RSリソースクラスの単体テスト

Jersey TestでJAX-RSリソースクラスの単体テストするときに、Mockitoでモックのビジネスロジックを差し込みます。

環境

  • Payara Web ML 4.1.1.161
  • Jersey 2.22.1

pom.xml

  • JUnit
  • Jersey Test
  • Jersey Testの実行環境
  • Mockito

が必要です。

jersey-test-framework-provider-grizzly2を依存性に含めると、Jersey Testと実行環境が両方入ります。

    <properties>
        <jersey.version>2.22.1</jersey.version>
        <jee-webapi.version>7.0</jee-webapi.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>${jee-webapi.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>1.10.19</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

テスト対象のリソースクラスなど

package sample;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/api")
public class MyApplication extends Application {
     // 中身は空
}
package sample;

import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.MediaType;

@Path("/hello")
@Produces(value = MediaType.APPLICATION_JSON)
@RequestScoped
public class HelloResource {

    // テスト時はここをモックに差し替える
    @Inject
    private HelloLogic helloLogic;

    @GET
    public Response getAll() throws Exception {
        List<HelloDto> list = helloLogic.selectAll();
        return Response.ok(list).build();
    }
}
package sample;

public class HelloDto {
    private String name;
    // setter/getter/コンストラクタ省略
}
package sample;

import java.util.List;
import javax.enterprise.context.RequestScoped;

@RequestScoped
public class HelloLogic {
    public List<HelloDto> selectAll() {
        // 本来は何らかの複雑な処理があると考えてください
        return Arrays.asList(new HelloDto("AAA"), new HelloDto("BBB"), new HelloDto("CCC"));
    }
}

テストクラスの作成

package sample;

import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.utilities.Binder;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.Mockito.*;

public class HelloResourceTest extends JerseyTest {

    /**
      * HelloLogicのモックのファクトリークラス
      */
    private static class MockHelloLogicFactory implements Factory<HelloLogic> {
        @Override
        public HelloLogic provide() {
            // mock() / when() / thenReturn() はMockitoのメソッド
            HelloLogic mockLogic = mock(HelloLogic.class);
            when(mockLogic.selectAll()).thenReturn(Arrays.asList(
                    new HelloDto("仮のA"),
                    new HelloDto("仮のB")
            ));
            return mockLogic;
        }

        @Override
        public void dispose(ProductQueryDao instance) {
            // 何もしない
        }
    }

    @Override
    protected Application configure() {
        Binder binder = new AbstractBinder() {
            @Override
            protected void configure() {
                // bindFactory() + to() でモックに差し替え
                bindFactory(MockHelloLogicFactory.class)
                        .to(HelloLogic.class);
            }
        };
        return new ResourceConfig()
                .packages(true, MyApplication.class.getPackage().getName())
                .register(binder);
    }

    @Test
    public void 全件取得でき件数が2件() {
        List<HelloDto> list = target("hello")
                .request()
                .get(new GenericType<List<HelloDto>>(){});
        assertThat(list.size(), is(2));
    }
}

ポイントは、ファクトリークラスを作成することと、bindFactory()メソッドを利用すること。

下記の@backpaper0さんの資料にあるbind()メソッドは、Javadoc

Does NOT bind the service type itself as a contract type.

とあり、Mockitoで作ったmockLogicを引数に指定すると、例外でテストがこけた。

bind()の引数はインタフェースのみ指定可能なのかも?

参考資料

Mockito 初めの一歩 - Qiita

java - Mocking EJB's with Mockito and Jersey Test Framework - Stack Overflow

http://backpaper0.github.io/ghosts/jaxrs-getting-started-and-practice.html#/12/8

AbstractBinder (HK2 API module 2.2.0-b11-SNAPSHOT API)

Factory (HK2 API module 2.2.0-b11-SNAPSHOT API)

指定したフォルダ配下の全.javaファイルのタブをスペースに一括変換する

$ find . -name "*.java" -exec perl -i.bak -pe 's/\t/    /g' "{}" \;

こうするとタブがスペース4つに変換はされるが、「Xxx.java.bak」というファイルが大量にできるので、これを一括削除。

$ find . -name "*.bak" -delete

参考URL

java - Change all tabs with whitespaces in IntelliJ for 10K+ classes - Stack Overflow

MacWiki - UNIXの基本コマンド