Mayaaでm:idの組み方が原因でページ全体がエラーになるのを防ごう(CompiledScript / ScriptEnvironmentの拡張例) #mayaa

これは Mayaa Advent Calendar 2015 の14日目です。昨日は「Spring BootのテンプレートエンジンにMayaaを使おうとしてみるリベンジ編」でした。

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

CompiledScript / ScriptEnvironmentの拡張例

Mayaaでページを作っていると、次のようなエラーに遭遇すると思います。

org.seasar.mayaa.impl.cycle.script.rhino.OffsetLineRhinoException: TypeError: Cannot call method "equals" of null in script=

これは、m:idの中に書かれたRhinoスクリプト実行時にExceptionが発生したという意味です。

これがどのようなときに起こるかというと、例えば

こんなMayaaファイルで

<!--商品を繰り返します-->
<m:forEach m:id="LOOP_ITEM" var="item" items="items">
  <m:echo><m:doBody /></m:echo>
</m:forEach>
<!--LOOP_ITEMで繰り返されるアイテムの名称を出力します-->
<m:write m:id="ITEM_NAME_HERE" value="${item.getName()}" />

こんなテンプレートを書いた時

<span m:id="ITEM_NAME_HERE">アイテム名</span>
<div m:id="LOOP_ITEM" >
<span m:id="ITEM_NAME_HERE">アイテム名</span>
</div>

本来であればLOOP_ITEMの内側に書かなければいけないITEM_NAME_HEREを外に書いてしまいました。

このようなことは、デザイナーさんか、サイト管理者の方が自らテンプレートを編集した時に起こります。

これが起こると、エラー画面が表示されてしまいます。

また、発生した例外には、mayaaファイルの行番号が書いてありますが、それはシステムログを見ない限り読めませんので、デザイナーがこれを見た時、ただただパニックになるしかありません。

これではいけません。

エラーが起きてもとりあえず画面を出す

多分世の中に初めて公開すると思います。次のカスタマイズによって、このような前述の事態を防ぐことが出来ます。

public class EbisuScriptEnvironmentImpl extends ScriptEnvironmentImpl {

    @Override
    protected CompiledScript compile(ScriptBlock scriptBlock, PositionAware position, int offsetLine) {
        if (scriptBlock.isLiteral()) {
            return super.compile(scriptBlock, position, offsetLine);
        }

        return new MyCompiledScriptWrapper(super.compile(scriptBlock, position, offsetLine));
    }
}
public class MyCompiledScriptWrapper implements CompiledScript {

    CompiledScript inner;
    public MyCompiledScriptWrapper(CompiledScript compiledScript) {
        inner = compiledScript; 
    }
    public void setExpectedClass(Class expectedClass) { inner.setExpectedClass(expectedClass); }
    public Class getExpectedClass() { return inner.getExpectedClass();  }
    public String getScriptText()   { return inner.getScriptText(); }
    public boolean isLiteral()      { return inner.isLiteral(); }
    public Object execute(Object[] args) {
        try {
            return inner.execute(args);
        } catch (OffsetLineRhinoException | MyOffsetLineScriptException e) {
            e.printStackTrace(System.err);            
            return "**** TEMPLATE ERROR!!! ****";
        }
    }
    public void setMethodArgClasses(Class[] methodArgClasses) { inner.setMethodArgClasses(methodArgClasses); }
    public Class[] getMethodArgClasses() { return inner.getMethodArgClasses(); }
    public boolean isReadOnly()     { return inner.isReadOnly(); }
    public void assignValue(Object value) { inner.assignValue(value); }
}

org.seasar.mayaa.provider.ServiceProviderに以下の記述を追加します

    <scriptEnvironment class="jp.example.MyScriptEnvironmentImpl">
        <scope class="org.seasar.mayaa.impl.cycle.scope.ParamScope"/>
        <scope class="org.seasar.mayaa.impl.cycle.scope.HeaderScope"/>
        <scope class="org.seasar.mayaa.impl.cycle.scope.BindingScope"/>

        <!-- "_" = current - page - request - session - application -->
        <scope class="org.seasar.mayaa.impl.cycle.script.rhino.WalkStandardScope"/>
        <!-- extension: java.lang.System.getProperty()
        <scope class="org.seasar.mayaa.impl.cycle.scope.EnvScope"/>
        -->
        <parameter name="wrapFactory" value="org.seasar.mayaa.impl.cycle.script.rhino.WrapFactoryImpl"/>
    </scriptEnvironment>

このようにすると、上記の記述では

**** TEMPLATE ERROR!!! ****
アイテム1
アイテム2
アイテム3

のように出て、とりあえず事故は回避されました。

画面にエラーがあった箇所の行番号を出力する

ただ、このままだとどこでエラーが起きたかわからなくなります。このままだと

変なゲジゲジがでたー!!!Σ(゚∀゚ノ)ノキャー

と言われてしまいます。(言われません)
そこで、後は簡単なテクニックになります。

public Object execute(Object[] args)
の中でテンプレートのファイル名、行番号は次のようにして取得できます。

String pageName = CycleUtil.getServiceCycle().getOriginalNode().getSystemID();
int lineNumber = CycleUtil.getServiceCycle().getOriginalNode().getLineNumber();
String message = errorMessage + "(" + pageName + ":" +  lineNumber +  ")";

あとは、この行番号と、exceptionのgetMessage()などを、CycleUtil.getRequestScope().set/getAttribute()を使って格納していって、default.mayaaにm:idを作って、テンプレートエラーが起きたら画面の上からオーバーレイしてきて表示する仕掛けなどを作るのが良いと思います。

error

この他便利な使い方

CompiledScriptの便利使い方として、他によく使う技としては、よく使うスクリプトをJavaScriptコンパイルせずショートカットして高速化を図る技があります。

その他、Rhino以外のスクリプトエンジンに切り替えることも可能です。

そこまでガチなことをやらなくても、ログに使って調査をしたりすることから始めても良いかもしれません。

結構ワクワクしますね。

まとめ

Mayaaにはこのようにわくわくする拡張ポイントがいっぱいあります。やり過ぎは厳禁ですが、今回紹介したように、トラブルを防ぐ手段として活用すると良いと思います。

コメントを残す