tohokuaikiのチラシの裏

技術的ネタとか。

Confluenceプラグイン開発でRenderer Componentモジュールが既に無効らしい件

2日近くかかってしまったのだけど、どうやらできないってことでF.Aらしい。

何がしたかったかというと、Confluenceのページ内容をFilterしたかった。

PHPでいうところの、ob_start()のコールバック的な。そんなFilterくらいあるんじゃないかと思ったのだけど無かった。いや、正確には昔あったらしい。あったと思われる。Confluence2.8~3.5までは。

Renderer Component Moduleのドキュメント読むとそれっぽいことができるみたい

Renderer Component Module - Confluence Development - Atlassian Developer Documentation
要は、wiki文字列からContextまでの変換を各コンポーネントに渡してるからそれを順繰りに処理していって、その間に自前のコンポーネントいれることができるよ。ただし、これは非常にナイーブな処理で地雷が多いから気を付けてね。っていう。

Documentを読んで、実際のやり方について

renderer-componentで処理対象となるcomponentを指定する。なので、atlassian-plugin.xmlには

<component key="fooRenderer" name="foo test renderer" class="example.FooRenderer"/>
<!-- Renderer -->
<renderer-component key="foo-renderer-component"
    class="com.atlassian.confluence.renderer.plugin.SpringRendererComponentFactory"
    weight="100">
    <param name="componentName">fooRenderer</param>
</renderer-component> 

って書く。これにより、

  1. SpringRendererComponentFactoryによりcomponentが生成
  2. 生成されるComponentは、componentNameパラメタで指定したComponentキーを持つもの
  3. 上記のComponentに対応するJavaクラスFooRendererのrenderメソッドWiki文字列を自在に変換

という流れ。

ちなみに、既にConfluenceのCore側では
confluence-project/confluence-core/confluence/src/etc/java/plugins/wiki-renderer-components.xml
によって、20以上のRendererが登録されているのが分かる。そのweightは、1000~21000なのでそれより小さいWeightを渡せば最初に処理されるし、それより大きいWeightを指定すれば最後に処理が掛かる。

Java

SpringRendererComponentFactoryは書く必要がない。もちろん、自前のFactoryクラスを作ってもいいのだけど、さすがにそこまで面倒なことはしない。。

Componentクラスだけ書く。なお、そのComponentは上記のDocumentによるとRendererComponentクラスをimplementsしている必要がある。

という事で、こんな感じ。

package example;

import com.atlassian.renderer.RenderContext;
import com.atlassian.renderer.v2.RenderMode;
import com.atlassian.renderer.v2.components.RendererComponent;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

public class FooRenderer implements RendererComponent {

    public FooRenderer() {
        System.out.println("renderer constructor");
    }

    @Override
    public boolean shouldRender(RenderMode rm) {
        return true;
    }

    @Override
    public String render(String wiki, RenderContext context) {
        System.out.println("renderer render");
        return wiki;
    }
}

だが、ダメだ。

確かに、FooRendererのインスタンスは生成された。
具体的には、プラグインのインストール時。OSGiプラグインってそういうものなんだろうか?共通の場所に1つインスタンスを生成して、他のプラグインなどで共通できるというイメージがある。

まぁ、それはさておき。

Confluenceのページを表示する際に、FooRenderer::render()が走るのかと思いきや、全然そんなこともなかったり。もちろん、shuldRenderメソッドだってかすりもしない。

それどころか、ダッシュボードを開こうとした際に何やらNullポインタエラーが出る。

java.lang.RuntimeException: Error rendering template for decorator root
    at com.atlassian.confluence.setup.velocity.ApplyDecoratorDirective.render(ApplyDecoratorDirective.java:191)

caused by: java.lang.RuntimeException: Error rendering template for decorator global
    at com.atlassian.confluence.setup.velocity.ApplyDecoratorDirective.render(ApplyDecoratorDirective.java:191)

caused by: org.apache.velocity.exception.MethodInvocationException: Invocation of method 'renderConfluenceMacro' in class com.atlassian.confluence.themes.GlobalHelper threw exception java.lang.NullPointerException at /decorators/global.vmd[line 60, column 29]
    at org.apache.velocity.runtime.parser.node.ASTMethod.handleInvocationException(ASTMethod.java:337)

caused by: java.lang.NullPointerException
    at com.atlassian.confluence.renderer.plugin.SpringRendererComponentFactory.getComponentInstance(SpringRendererComponentFactory.java:25)

って感じ。SpringRendererComponentFactory.javaの25行目って、

    public RendererComponent getComponentInstance(Map map)
    {
        if (!map.containsKey("componentName"))
            throw new IllegalArgumentException("Required parameter missing: componentName. Paramters are: " + map);

        String componentName = (String) map.get("componentName");
        Object component = beanFactory.getBean(componentName);

        if (component == null)
            throw new IllegalStateException("Renderer component with name " + componentName + " not found in spring context");

        return (RendererComponent) component;    
    }

ってあ感じで、beanFactory.getBean(componentName)しているところ。getBeanできてないんじゃん。OMG!
ちなみに、componentNameって変数には、atlassian-plugin.xmlのrenderer-component内のparam[name="componentName"]で指定したものが入る。


たぶん、Confluence4で使えなくしたんだろうな。。。

上記のドキュメントは、Confluence2.8から使えるよって書いてあるけど、同時に警告文も書いてあって、要約すると

Wikiのパーサは非常にデリケートでナイーブだから、ここに手を突っ込んであれこれやるととんでもないところに影響出たりとかして、やんない方が良いよ。そもそもこれはConfluenceコアの開発者のためのプラグインモジュールだしね。まぁ自己責任でね」

って感じ。

Confluence5のコードを見ると、Wikiの頃に比べてデリケートでナイーブじゃなくなったXHTMLだろうに無茶気を使って処理してる。Firebugとかで余計な属性値やこっそりHTMLタグを入れてもみんな除去されるのはこのサニタイズ処理のおかげ。

これを見るにつけ、Confluence3の時代にWikiのパーサを頑張って書いて、その結果「もーいやだ・・・。この無間地獄」と思った開発者の心の叫びが聞こえる。そして、XHTMLベースのConfluence4のエディタを必死こいて作った理由も納得できる。Wikiパーサの無間地獄に比べれば、見通しの良いXHTMLを扱えるのなら、リッチエディタを開発する方がなんぼか人類の為になるし開発者のモチベーションも段違いってことを。