Category Archives: Java

Mayaaに慣れた僕がMixer2を試してみる

これは Mayaa Advent Calendar 2015 の17日目です。昨日は「Mayaaで独自Processorを作ろう!」でした。

寒くなってきました。いよいよクリスマスまでカウントダウンですね!

毎日書けるのか心配だったこのブログも残すところあと1桁となり、終わってしまうことがちょっとさみしく思います。とはいえ、一日たりとも中断せぬよう、最後まで引き締めて行きます。

なお、Mayaaアドベントカレンダーはまだ参加者を募集しています!

Mayaaに慣れた僕がシリーズ開始

さて、今日から、Mayaa以外のテンプレートエンジンを試すシリーズを始めたいと思います。

Mayaa以外にもデザイナーとプログラマーの協業をうたったテンプレートエンジンは多数あります。それらを実際に試してみて、3日目から紹介した「いしがみメソッド」を適用できるか検証していきます。

第一回はMayaaの影響を受けて開発されたと言われる、Mixer2を試してみたいと思います。

Mixer2とは

Mixer2

Mixer2はJavaアプリケーション用テンプレートエンジンです。テンプレートはXHTMLで書きます。 100% pureな、XHTMLとCSSです。(html5もXML構文で書けば使えます)
と掲げられています。

テンプレートをXHTMLで書くという点がMayaaと共通しています。Mayaaと異なる点は、mayaaファイルというような独自の定義ファイルは存在せず、すべてJavaで制御することです。すべてJavaにすることでフロントのJUnitテストを可能であることをうたっています。

HelloWorldしてみる

Mixer2はMavenを使うことを推奨しているようです。jarファイル単体の配布はここでしているようですが、古いバージョンで止まっています。

POMに以下を追加しましょう。

<dependencies>
  <dependency>
    <groupId>org.mixer2</groupId>
    <artifactId>mixer2</artifactId>
    <version>1.3.2</version>
  </dependency>
</dependencies>

Mixer2 - TIPSによると、Mixer2EngineはSingletonとして扱うべきのようですので、はじめからSingletonにしておきます。

public class Mixer2EngineSingleton {
    private static Mixer2Engine m2e = new Mixer2Engine();
    public static Mixer2Engine get() {
        return m2e;
    }
}

テンプレートファイルを/WEB-INF/view/hello.xhtmlにおいてみます。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>たいとる</title>
</head>
<body>
<span id="HELLO_HERE">ハローワールド</span>
</body>
</html>

mixer2はm:idのような独自の名前空間ではなく、id属性を使うことを推奨しています。id属性だと複数使い回すことができないので、m:idのような使い方をするなら、class属性を使うことになるかもしれません。

また、XHTMLの文法上、titleタグにid属性を付けることは正しくないので、この部分も悩ましいところではあります。

そして、以下のようなServletを書けば、画面に「こんにちはMixer2」と表示されます。

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            Mixer2Engine m2e = Mixer2EngineSingleton.get();
            Html html = m2e.loadHtmlTemplate(getServletContext().getResourceAsStream("/WEB-INF/view/hello.xhtml"));
            html.getById("HELLO_HERE", Span.class).replaceInner("こんにちはMixer2");
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html");
            response.getWriter().print(m2e.saveToString(html));
        } catch (TagTypeUnmatchException e) {
            e.printStackTrace(response.getWriter());
        }
    }
}

Mayaaと違って、Servletと疎結合になっています。そのため、文字コードやContentTypeなどは自分で指定してあげる必要があります。

Mixer2でいしがみメソッドを適用する

m:id含め、プログラム都合の識別子は常に大文字を使う

これは、プログラム制御用のid, class属性を常に大文字にすることで対応できます。

m:idのする仕事を4種類に限定する

同上です。ただし、id属性は、テンプレート内で一度しか使えないので、class属性を使うことが多くなると思います。

class属性を使った場合、getDescendantsで検索をするのですが、常に全DOMを走破するとオーバーヘッドが大きいかもしれません。

(Mayaaの場合、DOMを使わずSAXのように先頭からトラバースしていく方式なので、この問題がない一方xpath指定の効率が非常に悪くなっています。)

4種類の実装方法はルールに従う

これらは、毎回タグ操作をゴリゴリ書くのではなく、ある程度ユーティリティ化することによって、定型化することができると思います。

LOOP系のm:idはindexには長い名前を使い、maxを十分に大きな値を設定する

Mixer2の場合はプログラミングをJava内のスコープで行うのでこのような問題はありません。

writeプロセッサーでエスケープを解除するときは全部する

replaceInnerメソッドは常にエスケープしてしまうみたいなので、エスケープして欲しくない場合は別の方法が必要になりそうです。

tableタグだけは特別に扱う

これは、テンプレートエンジンというより、HTMLの仕様上の問題点なので、Mixer2でも同じ課題があります。

m:idをパラメータ対応にする

タグの属性は getOtherAttributes() で取得できます。こんな感じで取得することができます。

span.getOtherAttributes().get(new QName("number"))

PathAdjusterを効果的に使う

Mixer2にもPathAdjusterがあります。仕様はMayaaのそれに似ているようです。

ヘッダー・フッター・共通部品はiframeタグをうまく使う

この部分は部分マーシャルを使ってうまく行くのではないかと思います。

テンプレート上のコメントは、ソース表示時に見えないようにする

Mixer2ではそもそもコメントが全部消えます!

単語はなるべくテンプレート側に書く

これは問題ないように思います。

nekoHTMLパーサーをいじる

初めからHTML5対応です。

まとめ

触ってみた感想として、Mayaaよりも、原始的な単機能のテンプレートエンジンという感じがしました。そのまま使っても、デザイナーと協業まで持っていくのは厳しいと思います。

また、タグの構造に対して厳密なコードを書く必要があるので、出来上がったテンプレートのタグの構造を後からプログラムの変更なしに変更するということには制約が多いように思います。

Mixer2を更にベースにして、独自フレームワークを構築することが必要になってくると思われます。

Mixer2の良い点は逆に、Servletと結合がないために単体で動いてしまうことです。サーバーサイド側でHTMLを整形するならJavaとの相性が非常によく、型安全でテスタブルにコーディングできるのは嬉しいです。

全体のテンプレートエンジンとして使うより、システムにmixer2を読み込んでおいて、HTMLを部分的に整形したり、Mayaaと組み合わせて部品をMixer2で作るといったこともというような合わせ技も検討の余地があるかもしれません。

思ったよりもMayaaと競合しない技術ではないかと思いました。

Mayaaで独自Processorを作ろう!

これは Mayaa Advent Calendar 2015 の16日目です。昨日は「Mayaaでサイトを作るときのワークフロー」でした。

今日も11日目の続編を書きます。

とは言え、PathAdjusterの書き換えについては
5日目
SourceDescriptorについては
昔の記事
InjectionResolverについては、
これも昔の記事
で紹介していました。

そうすると、残っているのはProcessorです。

独自のProcessorを作ろう

独自のプロセッサーが必要になるのはどういう時でしょうか?
例えば、こんな時だと思います。
- 毎回Mayaaファイルに同じような記述をしている。もっと楽をしたい!
- Mayaaファイルに"${ }"でJavaScriptを書いたりせず、独自記法で直接Javaオブジェクトにアクセスして高速化を図りたい

思いつくのはこんな感じです。

Processorを極めると、Mayaaを極めるといった感じがします。

独自Processorの作り方

プロセッサーの登録には、mldというファイルが必要になります。これは、そのプロセッサーがどんな属性を受け取るのかなどを設定します。

記述例は、Mayaaのソースコード 内にある、
/mayaa/src-impl/org/seasar/mayaa/impl/engine/processor/mayaa.mld
を見てみるのが一番わかり易いと懐います。

ということで、独自のmldファイルを作って、my.mldとしてみます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE library
    PUBLIC "-//The Seasar Foundation//DTD Mayaa Library Definition 1.0//EN"
    "http://mayaa.seasar.org/dtd/mld_1_0.dtd">
<library uri="http://example.jp/mymayaa">
    <description>My Mayaa processors</description>
    <processor name="if" class="jp.example.MyIfProcessor">
        <property name="test" expectedClass="boolean"/>
    </processor>
    <processor name="write"
            class="jp.example.MyWriteProcessor">
        <property name="value" expectedClass="java.lang.String"/>
        <property name="escape" expectedClass="boolean" default="false"/>
    </processor>
</library>

この例では、writeプロセッサーとifプロセッサーを独自拡張しようとしています。

このファイルはどこに置けばよいでしょうか?
こちらの情報によると

  WEB-INF/lib
  jar内 META-INF/
  classes/以下全ディレクトリ

などに置けば有効になります。

とのことです。

ということで、classes/META-INFなどのディレクトリを切って、
org.seasar.mayaa.provider.ServiceProvider
まどと一緒に入れてしまうのが良いと思います。

Processorを実装する

プロセッサーの実装例は、すでにあるプロセッサーのソースを読むのが一番早いです。

今回はこんな感じのプロセッサーを作ってみたいと思います。

  • my:writeプロセッサー

    • escapeXml, escapeEol, escapeWhitespaceを毎回書くのはめんどくさいから、escape='false'で全部指定出来るようにする
  • my:ifプロセッサー

    • といちいち囲まなくても同じ動きをする
    • テンプレート側にm:NOT=""を付けることで条件を反転して動作する

これらを実装すればMayaaファイルを一気にすっきりさせられそうです!

実装例(MyWriteProcessor)

public class MyWriteProcessor extends org.seasar.mayaa.impl.engine.processor.WriteProcessor {
    public void setEscape(ProcessorProperty escape) {
        setEscapeEol(escape);
        setEscapeWhitespace(escape);
        setEscapeXml(escape);
    }
}

実装例(MyIfProcessor)

public class MyIfProcessor extends org.seasar.mayaa.impl.engine.processor.IfProcessor {

    private volatile boolean created = false;

    private ProcessorProperty _test;

    // MLD property, expectedClass=boolean
    public void setTest(ProcessorProperty test) {
        if (test == null) {
            throw new IllegalArgumentException();
        }
        _test = test;
        super.setTest(_test);
    }

    @Override
    public ProcessStatus doStartProcess(Page topLevelPage) {
        if (!created) {
            synchronized (this) {
                if (!created) {
                    ProcessorTreeWalker[] childProcessors = new ProcessorTreeWalker[getChildProcessorSize()]; 
                    for (int i = 0; i < childProcessors.length; i++) {
                        childProcessors[i] = getChildProcessor(i);
                    }
                    clearChildProcessors();
                    ProcessorTreeWalker echoNode = createNode(this, "echo");
                    for (int i = 0; i < childProcessors.length; i++) {
                        if (childProcessors[i] != null) {
                            echoNode.addChildProcessor(childProcessors[i]);
                        }
                    }
                    echoNode.addChildProcessor(createNode(this, "doBody"));
                    addChildProcessor(echoNode);
                    created = true;
                }
            }
        }
        if (_test == null) {
            throw new IllegalStateException("test attribute is empty." + getInjectedNode().getSystemID() + " (" + getInjectedNode().getLineNumber() + ")");
        }
        boolean test = ObjectUtil.booleanValue(_test.getValue().execute(null), false);

        NodeAttribute attr = getOriginalNode().getAttribute(SpecificationUtil.createQName("NOT"));
        if (attr != null) {
            test = !test;
        }

        return test ? ProcessStatus.EVAL_BODY_INCLUDE : ProcessStatus.SKIP_BODY;
    }

    static ProcessorTreeWalker createNode(TemplateProcessor node, String name) {
        QName qname = QNameImpl.getInstance(name);
        LibraryManager libraryManager = ProviderUtil.getLibraryManager();
        ProcessorDefinition def = libraryManager.getProcessorDefinition(qname);
        SpecificationNodeImpl echo = new SpecificationNodeImpl(qname);
        TemplateProcessor proc = def.createTemplateProcessor(node.getOriginalNode(), echo);
        proc.setOriginalNode(node.getOriginalNode());
        proc.setInjectedNode(echo);
        return proc;
    }
}

Ifの方が結構複雑な感じですね。これは、複数のプロセッサーの仕事をまとめるために、初めてレンダリングするときに、前後にプロセッサーを足しています。

さすがにもう少しスマートなやり方もあるかもしれません。

記述例

Mayaaファイル先頭のxmlns属性に、今回使った名前空間を追加しないと使えません。今回はhttp://example.jp/mymayaaというものを書きましたが、実際は自分が所属するドメインなどユニークなものを使ってください。

<?xml version='1.0' encoding='UTF-8' ?>
<m:mayaa xmlns:m="http://mayaa.seasar.org" xmlns:my="http://example.jp/mymayaa" >
  <!-- XXXの時にのみ表示します -->
  <my:if m:id="IF_XXX" test="${request.XXX}" />
  <!-- YYYを出力します。 -->
  <my:write m:id="YYY" escape="false" />
</m:mayaa>
<div m:id="IF_XXX">XXXです</div>
<div m:id="IF_XXX" m:NOT="">XXXではありません</div>
<span m:id="YYY_HERE">YYYを出力</span>

まとめ

このように、プロセッサーは簡単に作れます。上手くやると、Mayaaファイルをとてもすっきりさせたり、独自のテンプレート記法を自由に生み出せることが出来るでしょう。

MyIfProcessorと同じような仕掛けで、for, forEachプロセッサーは簡単に作れます。

Mayaaファイルの書き方がかったるいなーなどと思ったら、Processorの作成に挑戦することをおすすめします。

おまけ:じつはMayaaは何でもプロセッサーで処理する

Mayaaの内部構造を理解すると、例えば「静的文字列を出力する」(LiteralCharactersProcessor)もプロセッサーだったりします。&lt;!CDATA句もCDATAProcessorというのがあったりします。

Mayaaのレンダリングはプロセッサーであることを理解しておくと、何かあった時の調査に役立つと思います。

Mayaaでサイトを作るときのワークフロー

これは Advent Calendar 2015 の15日目です。昨日は「Mayaaでm:idの組み方が原因でページ全体がエラーになるのを防ごう(CompiledScript / ScriptEnvironmentの拡張例)」でした。

今日を乗り切ればあと10日です。最後まで気を抜かず頑張ります!といいつつ、昨日もガチなこと書いてしまったので、今日はゆるく行きましょう。

Mayaaを使ったサイトの作り方

Mayaaを使ってサイトを作るってどんな感じになるんでしょうか。

HTMLを先に書くのが良いのでしょうか?Mayaaを先に書くのが良いのでしょうか?

今日はそんな話をしてみようと思います。

プログラマーが書くときは先にHTMLを書くと良い

意外かもしれませんが、最初にプロトタイプを書くのはプログラマー側のほうが良かったりします。まずは、m:idも何もない状態で、HTMLで骨組みを作ります。

<div m:id="LOOP_ITEM">
名前: <span m:id="NAME_HERE">名前</span><br />
住所: <span m:id="ADDRESS_HERE">東京都</span><br />
</div>
<div m:id="DUMMY_TAG">
名前: <span m:id="NAME_HERE">名前</span><br />
住所: <span m:id="ADDRESS_HERE">東京都</span><br />
</div>
<div m:id="DUMMY_TAG">
名前: <span m:id="NAME_HERE">名前</span><br />
住所: <span m:id="ADDRESS_HERE">東京都</span><br />
</div>

<br />とか書いてるし、デザイナー・HTMLコーダーの方に言わせたら怒られるかもしれませんが、僕達が作るのはデザインではないのでこんな感じで骨組みを作っていきます。

そして、Mayaaファイルを作って動きを見ます。シンプルなデザインの方が結局ロジックを考えやすいです。

Mayaaはデザインを凝るためのテンプレートエンジンですが、デザインを放棄するために使うという発想です。

デザイナーが最後まで作るとどうなるか

残念ながらサイトを作る能力はデザイナー氏の方が上です。プログラマーって詰めが甘いんすよね。反省します。

なので、デザイナーが動くよりも前に必要な機能、m:idは全部そろえてしまって、デザインの人にグリグリいじってもらった方が良いサイトができます。

ただ、この方法を取ると次の問題が

  • デザイナー段階で手戻りや追加要件が発覚する場合が多い
  • デザイナーにどこまでテストの負担を強いるのか

結局のところ、システム開発という観点だと、この問題が発生します。

伝統を駆使する

こういう時は、伝統的な発想が役立ちます。

デザイナーはワイヤーフレームを作り、フォトショップでデザインを作り、コーダーがHTMLを打ち込む。

SEが基本設計、詳細設計を詰め、プログラマーがプログラミングして、単体テスト、結合テスト。。。

おう、受託開発\(^o^)/ウォーターフォール/(^o^)\

経験上、このスタイルでも、Mayaaはまあ、うまく使えてるかなと思います。

そうすると、気づいてみると、一つの組織の中に2つのフローが発生してきます。

*ディレクター→デザイナー→コーダー
*PM→SE→PG→テスター

一つの組織に2つのフローが入り乱れて行く、この問題はどこでサバくべきなのか!結構複雑になってきます。

それは、嫌なことなのかといえば、それって良いことだと思います。

専門性を発揮する

僕の所属する組織では、SEはPGの上位職ではなく、それぞれが専門チームとして活動しています。デザインチームも、デザイナーとフロントエンドエンジニアが対等に活動しています。それぞれが専門性を発揮できていることは、良いことだと思います。

Mayaaによってデザインとコードが分離できていること、一つの雛形を何度も使いまわせることが、各自の専門性をアップすることに一役買っているかもしれません。

そうだったらいいな。

まとめ

1人や2人でサイトを使うのにも、Mayaaは便利です。また、大人数で作るのにも十分向いています。大事なことは、各々が自分の専門性を理解して、良いサイトを作ろうと、皆で力を合わせることだと思います。