JavaEE8 で仕様検討が進んでいる MVC1.0 (Model-View-Controller API 1.0 - JSR 371) の参照実装である Ozark は既に M2 が出ていて簡単に試すことができます。
テンプレートエンジンも Extension として、Mustache、Freemarker、Velocity、Thymeleaf などが提供されています。
以下の依存を追加すれば JavaEE7 の Glassfish4 で使うことができます。
compile 'org.glassfish.ozark:ozark:1.0.0-m02'
テンプレートエンジンに Thymeleaf を使いたい場合は Extension を追加するだけです。
compile 'org.glassfish.ozark.ext:ozark-thymeleaf:1.0.0-m02'
残念ながら、この Extension は Thymeleaf 2系です。
なお、ここに記載するのはマイルストーンリリース2についてですのでご注意ください。
Thymeleaf の Extension
Thymeleaf の Extension は大したことはやっていません。
Thymeleaf のリゾルバを作り、Thymeleaf のエンジンを返す、以下のようなプロデューサが定義されています。
@Produces @ViewEngineConfig public TemplateEngine getTemplateEngine() { TemplateResolver resolver = new ServletContextTemplateResolver(); TemplateEngine engine = new TemplateEngine(); engine.setTemplateResolver(resolver); return engine; }
そして、テンプレート処理を行う ViewEngine が以下のように定義されています。
@ApplicationScoped public class ThymeleafViewEngine extends ViewEngineBase { // ... @Override public void processView(ViewEngineContext context) throws ViewEngineException { try { HttpServletRequest request = context.getRequest(); HttpServletResponse response = context.getResponse(); WebContext ctx = new WebContext(request, response, servletContext, request.getLocale()); ctx.setVariables(context.getModels()); engine.process(resolveView(context), ctx, response.getWriter()); } catch (IOException e) { throw new ViewEngineException(e); } } }
Thymeleaf の Extension は、大きくはこれだけです。
ViewEngine の選択
Extension として定義された ViewEngine を Ozark がどのように扱っているかを見ると、以下のようになっています。
@ApplicationScoped public class ViewEngineFinder { @Inject @Any private Instance<ViewEngine> engines; private Map<String, ViewEngine> cache = new HashMap<>(); public ViewEngine find(Viewable viewable) { Optional<ViewEngine> engine; final String view = viewable.getView(); engine = Optional.ofNullable(cache.get(view)); if (!engine.isPresent()) { final Set<ViewEngine> candidates = new HashSet<>(); for (ViewEngine e : engines) { if (e.supports(view)) { candidates.add(e); } } engine = candidates.stream().max( (e1, e2) -> { // 省略(Priorityの高いエンジンを選ぶ) }); if (engine.isPresent()) { cache.put(view, engine.get()); } } return engine.isPresent() ? engine.get() : null; } }
@Inject @Any private Instance<ViewEngine> engines;
で CDI管理となっている ViewEngine のインスタンスが取れるので、サポートする拡張子の ViewEngine を探して、優先度の高いものを採用しています。
Fragments が上手く動かない
Thymeleaf を使う場合は、通常以下のようにリゾルバにホームとなるディレクトリや拡張子を設定して使うことが多いです。
templateResolver.setPrefix("/WEB-INF/views/"); templateResolver.setSuffix(".html");
しかし、Thymeleaf の Extension では特に設定を行っていないため、webapp のルートがホームとして設定されます。
MVC1.0 のコントローラでは、以下のように foo/createForm.html
といったビューのパス文字列を返します。
@Path("foo") @RequestScoped public class FooController { @Inject private Models models; @GET @Path("new") @Controller public String initCreationForm() { models.put("model", new FooModel); return "foo/createForm.html"; } }
このパス文字列は、Thymeleaf エンジンで処理する直前で、以下のように resolveView()
で解決されます。
engine.process(resolveView(context), ctx, response.getWriter());
resolveView()
は何をしているかというと、
protected String resolveView(ViewEngineContext context) { final String view = context.getView(); if (!hasStartingSlash(view)) { // Relative? final String viewFolder = getProperty(context.getConfiguration(), VIEW_FOLDER, DEFAULT_VIEW_FOLDER); return ensureEndingSlash(viewFolder) + view; } return view; }
Ozark 側で定義された(デフォルトは"/WEB-INF/views/")パスでテンプレートまでのパスを(Thymeleafの知らない所で)作っています。
なので、Thymeleaf 側で Fragments を定義する場合はファイルのパスをフルパスで定義する必要が出てきます。
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{/WEB-INF/views/fragments/layout.html :: layout}">
Thymeleaf の Extension は作った方が早い
Thymeleaf 3 系 向けに以下の2クラス作るだけなので早いです。
@Dependent public class DefaultTemplateEngineProducer { @Produces @ViewEngineConfig public TemplateEngine getTemplateEngine(ServletContext context) { ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(context); resolver.setPrefix(ViewEngine.DEFAULT_VIEW_FOLDER); TemplateEngine engine = new TemplateEngine(); engine.setTemplateResolver(resolver); return engine; } }
resolver.setPrefix(ViewEngine.DEFAULT_VIEW_FOLDER)
で Thymeleaf 側に "/WEB-INF/views/" を設定します。
@Dependent
は beans.xml で bean-discovery-mode="all"
で定義されている場合は不要です(CDI に拾ってもらえればなんでも良い)。
ViewEngine は以下のようにします。
@ApplicationScoped public class ThymeleafViewEngine extends ViewEngineBase { @Inject private ServletContext servletContext; @Inject @ViewEngineConfig private TemplateEngine engine; @Override public boolean supports(String view) { return view.endsWith(".html"); } @Override public void processView(ViewEngineContext context) throws ViewEngineException { try { HttpServletRequest request = context.getRequest(); HttpServletResponse response = context.getResponse(); WebContext ctx = new WebContext(request, response, servletContext, request.getLocale()); ctx.setVariables(context.getModels()); engine.process(context.getView(), ctx, response.getWriter()); } catch (IOException e) { throw new ViewEngineException(e); } } }
engine.process()
では、パスの解決を Thymeleaf 側に任せます。
これで 以下のように Fragments にフルパス指定しなくて良くなります。
<html xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/layout.html :: layout}">