DIVE INTO S2 .HACK : INCLUDE PART I

Seasar2に慣れ親しむためにS2Containerのコードをプチハックしてみました。 v(^^)v

  • S2Container間の依存関係(include)をDEPENDENCY-LOOKUPからDEPENDENCY-INJECTIONにしてみる。
    • S2Containerは、1つのdiconファイル*1に対し、1つのS2Containerインスタンスが生成される仕組みになっています。そこで、コンポーネント群を機能ブロック毎に複数のdiconファイルに分けて使用する場合*2、includeタグを使って依存する子コンテナを関連付けるわけです。具体的には、依存するdiconファイルのパスを指定することで、関連付けを行います。しかし、この指定方法は、DEPENDENCY-LOOKUPスタイルなので、依存関係を外部からコントロールしようとすると、工夫が必要*3になります。そこで、なんかいい方法ないかなぁ〜と、悶悶と考えた結果“依存関係…依存関係…依存関係の解決…!…DI*4があるぢゃぁないか!”となったわけです。
    • 現状の課題点は、依存関係が直接diconファイルのパスになっている点です。ここを別名にして、別名とパスのマッピングを別に定義する仕組みを作ってやればいいんじゃな〜い? … と、いうわけで、実験君のはじまりはじまり〜。
    • diconファイルとしては、こんな感じの定義で動いたらいいかなぁ〜というのをまず書いてみました。includeタグの表記形式を拡張する方法*5で対応してます。
      • と書いた場合には、定義のみを行う。
      • と書いた場合には、別名を使ってincludeを行う。
      • と書いた場合には、今までと同様に、指定パスでincludeを行う。
      • 同じ別名がincludeで複数定義されていた場合には、includeツリーをルートから辿る順序で、先勝ちのルールとする。
    • Node0 ← Node1 ← Node2a という依存関係。

■ org/seasar/framework/container/factory/IncludeTagHandlerResolverNode0.dicon


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd">

<!-- Alias definition section -->

<!-- Include section -->

<!-- Component section -->

component0
component1
component2
</component>
"Here is node0 container."

■ org/seasar/framework/container/factory/IncludeTagHandlerResolverNode1.dicon


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd">

<!-- Include section -->

<!-- Component section -->
"Here is node1 container."

■ org/seasar/framework/container/factory/IncludeTagHandlerResolverNode2a.dicon


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd">

<!-- Component section -->
"Here is node2a container."

    • で、I'm Test-Driven*6 なので、テストコードを書いてみます。*7

■ org/seasar/framework/container/factory/IncludeTagHandlerResolverTest.java


package org.seasar.framework.container.factory;

import java.util.List;

import junit.framework.TestCase;

import org.seasar.framework.container.S2Container;

public class IncludeTagHandlerResolverTest extends TestCase {

private static final String PATH = "org/seasar/framework/container/factory/IncludeTagHandlerResolverNode0.dicon";

public void setUp() {
IncludeTagHandler.clear();
}

public void testBasicSettings() throws Exception {
S2Container container = S2ContainerFactory.create(PATH);
container.init();
List list = (List) container.getComponent("collection");
for (int i = 0; i < list.size(); ++i) {
if (i == 2) {
assertEquals("node" + i, "Here is node" + i + "a container.", list.get(i));
} else {
assertEquals("node" + i, "Here is node" + i + " container.", list.get(i));
}
}
}
}

    • じゃ、ハッキングしまぁ〜す。オレンジ色部分が追加コード。

■ org/seasar/framework/container/factory/IncludeTagHandler.java


/*
* Copyright 2004-2006 the Seasar Foundation and the Others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package org.seasar.framework.container.factory;

import java.util.HashMap;
import java.util.Map;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.env.Env;
import org.seasar.framework.util.StringUtil;
import org.seasar.framework.xml.TagHandlerContext;
import org.xml.sax.Attributes;

/**
* @author higa
*
*/
public class IncludeTagHandler extends AbstractTagHandler {

private static final long serialVersionUID = 7770349626071675269L;

/**
* @see org.seasar.framework.xml.sax.handler.TagHandler#start(org.seasar.framework.xml.sax.handler.TagHandlerContext,
* org.xml.sax.Attributes)
*/
public void start(TagHandlerContext context, Attributes attributes) {
String path = attributes.getValue("path");
if (path == null) {
throw new TagAttributeNotDefinedRuntimeException("include", "path");
}
S2Container container = (S2Container) context.peek();
String condition = attributes.getValue("condition");
if (!StringUtil.isEmpty(condition)) {
Map map = new HashMap();
map.put("ENV", Env.getValue());
Object o = createExpression(context, condition).evaluate(container,
map);
if (!(o instanceof Boolean)) {
throw new IllegalStateException("condition:" + condition);
}
if (!( (Boolean) o).booleanValue()) {
return;
}
}

path = resolveOrRegisterPath(path);
if (path == null) {
return; // Alias-path mapping was Registered
}

S2ContainerFactory.include(container, path);
}


private static final Map aliasNameDefinitions = new HashMap();

public static void clear() {
aliasNameDefinitions.clear();
}

private String resolveOrRegisterPath(String path) {
if (!path.startsWith("@")) {
return path;
}
int nameSep = path.indexOf(':');
if (nameSep < 0) {
// Resolve path
String name = path.substring(1);
path = resolvePath(name);
} else if (nameSep == 1) {
// 'path' attibute value was specified like "@:....".
throw new IllegalStateException(
"Alias name of S2Container was empty.");
} else if (nameSep > 1) {
// Register alias-path mapping
String name = path.substring(1, nameSep);
path = path.substring(nameSep + 1);
registerPath(name, path);
path = null;
}
return path;
}

private void registerPath(String name, String path) {
if (aliasNameDefinitions.get(name) != null) {
return;
}
aliasNameDefinitions.put(name, path);
}

private String resolvePath(String name) {
String path = (String) aliasNameDefinitions.get(name);
if (path == null) {
throw new IllegalStateException("Alias name '" + name
+ "' was not defined.");
}
return path;
}
}

    • んで、テスト! RED RED RED RED RED RED RED RED GREEN! いいんじゃなぁ〜い?
    • こんどは、リーフノードのコンテナを交換してみます。
    • Node0、Node1のdiconファイルには、全く手を加えず、Test1で依存関係を変更する方向で…。
    • Test1 ← Node0 ← Node1 ← Node2b という依存関係。
    • 切り替え用*8のルートのdiconファイルはこんな感じ。

■ org/seasar/framework/container/factory/IncludeTagHandlerResolverTest1.dicon


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd">

<!-- Alias definition section -->

<!-- Include section -->


    • そして、切り替えられるリーフノードのdiconファイルがこれ。〜aが〜bになってます。

■ org/seasar/framework/container/factory/IncludeTagHandlerResolverNode2b.dicon


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd">

<!-- Component section -->
"Here is node2b container."

    • そして、I'm Test-Driven*9 なので、またまたテストコードを書いてみます。先のテストコードに以下のコードを追加します。


private static final String PATH1 = "org/seasar/framework/container/factory/IncludeTagHandlerResolverTest1.dicon";

public void testOverrideSettings() throws Exception {
S2Container container = S2ContainerFactory.create(PATH1);
container.init();
List list = (List) container.getComponent("collection");
for (int i = 0; i < list.size(); ++i) {
if (i == 2) {
assertEquals("node" + i, "Here is node" + i + "b container.", list.get(i));
} else {
assertEquals("node" + i, "Here is node" + i + " container.", list.get(i));
}
}
}

    • しょっしゃぁ!、DOテスト! RED RED RED RED RED GREEN! ふぅ。しばしのプログラマーズ・ハイ状態…*10
    • 以上でハック完了であります。
    • それから、このコードの課題としては、以下のようなところでしょうか…ま、プチハックということで。*11
      • diconファイルの別名とパスをstaticなMapに持っているため、リロードの際には、中身をクリアしてあげる必要がある。
      • 別名を定義する際の表記方法がイマイチ*12。includeタグに別の新規属性を追加した方が、きっと自然。
      • IncludeTagHandlerに追加したコードは、別クラス(Innerクラスでもいいかも)にした方がいいだろうなぁ。
      • スレッドセーフとか全く考慮してないです。
    • まだまだ課題はありそうですが、とりあえず、YATTAA!!
    • いや〜、code-readingしたり、こうやって、HACKしてみたりすると、少しずつですがS2の理解が深まる感じがしますね。それに大変勉強になります。S2開発者の方々に感謝!
    • その昔、パソコンサンデーという知っている人は知っている番組があったのですが、それに出てくるドクターパソコン*13の口癖『習うより慣れろ』が頭に浮かびました。真理ですきっと (^^v

あとがき

はじめは、MetaDefに別名とパスのマッピング情報を持たせようとしたのですが、includeタグの処理の時点では、S2Containerのrootとかchildrenが設定されていないようで、かなり試行錯誤+七転八倒したのですが、出来ませんでした…理解不足の可能性大…orz。結局…あれ?、Mapで別に持てばいいんじゃない?ということに気づき、この形になりました。*14 staticフィールではなくて、ルートのS2Containerのインスタンス変数とかに持てると、多少いいかなぁと思うのですが、私の今の実力では、これが限界でした orz. orz. orz. orz.

*1:ちなみにdiconファイル以外のリソースからS2Containerを構築する拡張性もあって、拡張子でコンテナビルダーの振舞いを制御出来るようになっています。

*2:業務アプリケーションなら普通はそうしますよね…^^;;;

*3:最近、この課題の解決策の1つとしてincludeタグに、condition属性が付きました v(^^)v これは便利ですね〜。けっこう待ち望んでいた人いるんじゃないかなぁ〜。

*4:Dependency Injection です念のため ^^;;

*5:初めは と書いたのですがDTD直したりしたくなかったので、姑息な手段を使用しました。正直このやり方はイケテナイと自分でも思います。ま、プチハックなので…と逃げてみる ^^;;;;;

*6:但し、気が向いたときに限る ^^;;

*7:当然、はじめに書いたコードにはバグがありました。IncludeTagHandler.clearが抜けてたり…

*8:というか上書き指定という感じかなぁ…Override?

*9:但し、気が向いたときに限る。っていうかクドイ!

*10:魂が抜け、画面を見つめてニヤニヤしている状態。他人に見られるととってもYABAI。

*11:はい、その通りです、自分の無力さに対する言い訳です。orz

*12:イマ3くらい?

*13:ちょっと「桃や」のキャラクターに似ていた、故・宮永好道さん[シャープ顧問]

*14:これに気づいたときにはかなり脱力しました。 orz