cypher256's blog

Pleiades とか作った

JSP、JSTL、EL で定数を使う

今回のプロジェクトでは定数クラスは設計書から自動生成されるようにしました。でもせっかく定数クラス作っても、JSP ではベタ書きしていたり、JSP で定数使うのはおかしいとか言うエラい人もいたりしますが、やっぱり JSP でも定数使いたい場面はあります。例は悪いですけど、こんな。

public interface Flg {
    String ON = "1";
    String OFF = "0";
}
<td>${Flg.OFF}</td>
とか
<c:if test="${hogeFlg == Flg.ON}">

今まで、定数クラスごとにマップを作ってアプリケーション・スコープに登録したりしていたのですが、面倒なので自動登録してしまおうというのが、JSTLConstantsRegister です。どこかで聞いた言い回しですけど。これはルート・パッケージ配下のインターフェースか public static の値を JSP から使用可能にするものです。

// JSTL 定数レジスター・クラス
public class JSTLConstantsRegister extends ComponentAutoRegister {

    // 定数として登録する対象の型
    private static final List<Class<?>> targetTypes = new LinkedList<Class<?>>() {{
        add(String.class);
        add(Integer.class);
        add(Long.class);
        add(Enum.class);
        add(Boolean.class);
    }};

    // コンストラクタ
    public JSTLConstantsRegister(NamingConvention namingConvention) {
        for (String rootPackageName : namingConvention.getRootPackageNames()) {
            addClassPattern(rootPackageName, ".*");
        }
        addReferenceClass(getClass());
    }

    // 指定されたクラスの定数をアプリケーション・スコープに登録
    @Override
    protected void register(String className) {
        Class<?> clazz = ClassLoaderUtil.loadClass(ResourceUtil.getClassLoader(), className);
        BeanMap beanMap = new BeanMap();
        for (Field f : clazz.getFields()) {
            if (f.getDeclaringClass() != clazz) {
                continue;
            }
            if (!targetTypes.contains(f.getType())) {
                continue;
            }
            int mod = f.getModifiers();
            if (Modifier.isPublic(mod) && Modifier.isStatic(mod)) {
                String key = f.getName();
                Object value = FieldUtil.get(f, null);
                beanMap.put(key, value);
            }
        }
        if (beanMap.size() > 0) {
            String shortClassName = ClassUtil.getShortClassName(className);
            ServletContext application = ServletContextUtil.getServletContext();
            application.setAttribute(shortClassName, beanMap);
        }
    }
}


あとは適当に app.dicon とかに。

<!-- JSTL 定数レジスターのコンポーネント登録 -->
<component class="hoge.framework.internal.JSTLConstantsRegister"/>

実際はショート・クラス名の重複や値の null チェックをしたほうがいいと思います。ちなみに BeanMap は指定したキーがない場合は IllegalArgumentException をスローしてくれます。あとルート・パッケージ配下以外のクラスを登録したい場合は、dicon で指定できるように汎用的に作るか、コンストラクタの中で直接登録します。例えば、ActionMessages の定数を使いたい場合は、こんな。

    // コンストラクタ
    public JSTLConstantsRegister(NamingConvention namingConvention) {
        for (String rootPackageName : namingConvention.getRootPackageNames()) {
            addClassPattern(rootPackageName, ".*");
        }
        addReferenceClass(getClass());
        // クラスを指定して追加
        register(ActionMessages.class.getName());
        register(・・・.class.getName());
    }
    <html:errors property="${ActionMessages.GLOBAL_MESSAGE}"/>

やっていることは非常に単純です。面倒なことは ComponentAutoRegister がやってくれています。面倒なことというのは Java が標準でロードされていないクラスを検索する機能を持たないため、定数登録したいクラスを自分で検索するとなると、それがどこにあるのか、ファイルとしてあるのか、jar や war にあるのかということを考慮しなければなりません。自分で実装したとしても検証が非常に困難です。更にこのクラス、ソースを見る限り S2Container に依存してなさそうなので、DI Container を使っていないプロジェクトでもライブラリ的な使い方ができそうです。ComponentAutoRegister を使うとクラスに対する色々な処理ができますね。そんな使い方すんなとか言われそうですけど。

JSP で Action に指定した roles を使う

SAStruts では Servlet コンテナの認証を使った場合、アクションの roles 属性を指定することにより、アクセス制御が可能です。これでセキュリティは守られます。でも実際のところ、その権限によって、画面にメニューやリンク、ボタンの表示/非表示を切り替える必要があるのがほとんどではないでしょうか? JSP でベタで権限判定をしても良いのですが、せっかく、アクションに roles 属性を指定しているなら、そこでロール定義は管理し、JSP ではそのアクション名とメソッド名で判定したいところです。こんな感じ。

<!-- show ファンクションで表示/非表示判定 -->
<c:if test='${x:show("hoge/index")}'>
    HogeAction の index 権限がある人のみ表示
</c:if>
// ファンクション・クラス
public class Functions {

    // 指定されたアクション/メソッドが現在のユーザに権限があるか判定
    public static boolean isShow(String actionMethod) {
        ActionRolesMap actionRolesMap = ActionRolesRegister.getActionRolesMap();
        List<String> roleList = actionRolesMap.get(actionMethod);
        if (roleList == null) {
            throw new IllegalArgumentException("指定されたアクションメソッド " + actionMethod
                + " は存在しません。" + actionRolesMap);
        }
        if (roleList.size() == 0) {
            Exception e = new IllegalArgumentException("指定されたアクションメソッド "
                + actionMethod + " は roles が指定されていません。" + actionRolesMap);
            return false;
        }
        User user = SingletonS2Container.getComponent(User.class);
        return roleList.contains(user.roleId);
    }
}

TLD は適当に作っておきます。あと、上記の ActionRolesMap をどこで作っておくかなのですが、最初は ActionCustomizer でやろうと思ったのですが、SAStruts の環境名が ct の場合、最初にすべてのアクションをロードしないため、別アクションの roles が参照できません。そこで結局また ComponentAutoRegister を拡張。こんな感じ。

// アクション・ロール・レジスター・クラス
public class ActionRolesRegister extends ComponentAutoRegister {

    private static String KEY = ActionRolesRegister.class.getName();

    // コンストラクタ
    public ActionRolesRegister(NamingConvention namingConvention) {
        for (String rootPackageName : namingConvention.getRootPackageNames()) {
            addClassPattern(rootPackageName + ".action", ".*Action");
        }
        addReferenceClass(getClass());
    }

    // @Execute の roles 属性からアクション・ロール・マップを作成
    @Override
    protected void register(String className) {
        Class<?> clazz = ClassLoaderUtil.loadClass(ResourceUtil.getClassLoader(), className);
        ServletContext application = ServletContextUtil.getServletContext();
        ActionRolesMap actionRolesMap = (ActionRolesMap) application.getAttribute(KEY);
        if (actionRolesMap == null) {
            actionRolesMap = new ActionRolesMap();
            application.setAttribute(KEY, actionRolesMap);
        }
        for (Method method : clazz.getMethods()) {
            Execute anno = method.getAnnotation(Execute.class);
            if (anno == null) {
                continue;
            }
            String[] roles = anno.roles().split("\\s*,\\s*");
            List<String> roleList = new LinkedList<String>();
            for (String role : roles) {
                if (StringUtils.isNotBlank(role)) {
                    roleList.add(role);
                }
            }
            String shortName = ClassUtil.getShortClassName(className);
            String actionName = shortName.replaceFirst("Action$", "");
            String actionMethod = actionName + "/" + method.getName();
            actionMethod = StringUtils.uncapitalize(actionMethod);
            actionRolesMap.put(actionMethod, roleList);
        }
    }

    // アクション・ロール・マップの取得
    public static ActionRolesMap getActionRolesMap() {
        ServletContext application = ServletContextUtil.getServletContext();
        return (ActionRolesMap) application.getAttribute(KEY);
    }

    // アクション・ロール・マップ
    public static class ActionRolesMap extends HashMap<String, List<String>> {
    }
}


あとは適当に app.dicon とかに。

<!-- アクション・ロール・マップのコンポーネント登録 -->
<component class="hoge.framework.internal.ActionRolesRegister"/>


・・・というか、書いてて気づきましたが、Functions.isShow で普通にアクション・クラスの Execute アノテーション取り出して、判定すればいいだけでした。それだと ActionRolesRegister なんてものは、いらないです。でも、先読みにより実行効率があがるかもしれないので良いということにしておきます。

toString

共通の親エンティティに toString を実装し public フィールド値を出力されるようにすれば色々便利です。これは特に S2JDBC のエンティティに限ったことではありません。

@MappedSuperclass
public class AbstractEntity implements Serializable {
    public String toString() {
        return <フィールドの値を連結した文字列>;
    }
}
public class Table1 extends AbstratEntity {
    public Long id;
    public String xxxCd;
    public String xxxNm;
}

Table1 table1 = ・・・;
log.debug("table1の中身:" + table1);
DEBUG 2008/01/01 table1の中身:{id=123, xxxCd=A01, xxxNm=名前}

通常はデバッガを使うので不要ですが、大量のデータを一度に見たり、ログを参照するしかない状況の場合に便利です。元々、toString メソッドがデバッグ用途に用意されたものなので、これはどこでもよくやっていると思います。ただ、<フィールドの値を連結した文字列> は手でやるのは面倒なので、自動生成の選択肢が色々あります。まず実行時に解決する動的なもの。

  • Jakarta Commons ToStringBuilder
  • S2 Beans


次に静的に toString メソッドを生成する Eclipse プラグイン

  • Commonclipse
  • Commons4E
  • JUtils


今回、S2 を使っているので、どうせなら、Beans を使おう(変な使い方ですけど)ということで、こんな。

@MappedSuperclass
public class AbstractEntity implements Serializable {
    public String toString() {
        return Beans.createAndCopy(Map.class, this).execute().toString();
    }
}

ちなみに Commons なら、こんな感じです。

        return ToStringBuilder.reflectionToString(this);


これで、早速動かすとスタックオーバーフロー。。。 というかエンティティは相互参照があるので、そりゃそうですね。Table1 -> Table2 -> Table1 -> Table2 -> のような感じで、toString の呼び出しが無限ループに。結局、ちょっとダサいですが、こんなコードにしました。

public String toString() {
    Map<String, Object> fieldMap = new TreeMap<String, Object>();
    for (Class<?> c = getClass(); c != Object.class; c = c.getSuperclass()) {
        for (Field field : c.getFields()) {
            int mod = field.getModifiers();
            if (Modifier.isStatic(mod)) {
                continue;
            }
            Object value = FieldUtil.get(field, this);
            if (value == null || value instanceof String
                    || value instanceof Integer || value instanceof Long
                    || value instanceof Boolean) {
                fieldMap.put(field.getName(), value);
            }
        }
    }
    return fieldMap.toString();
}

環境プロパティー

通常、業務システムを開発する場合、色々な設定値をどこかに格納しておく必要があります。DB だったり、プロパティー・ファイルだったりするのですが、今回のシステムではプロパティー・ファイルを使いました。そこで、環境による違いを吸収するために、勝手に切り替わる仕組みを作るか、原始的な方法では手で書き換えるなどがありますが、S2 には Ruby の実行モードのような環境名というものがあります。条件により dicon ファイルの差し替えが可能だったりしますが、環境ごとのプロパティーなどは設定が同じファイル内の近くにあるほうが見やすいので、こんな感じにしました。

[/src/main/resources/env.properties]
# 一時作業ルート・パス
TEMP_ROOT_PATH.ct=c:/temp
TEMP_ROOT_PATH.it=/tmp
TEMP_ROOT_PATH.product=/tmp

# ほげルート・パス
HOGE_ROOT_PATH.ct=c:/temp
HOGE_ROOT_PATH.it=/usr/hoge
HOGE_ROOT_PATH.product=/usr/hoge

プロパティー・ファイルのキー末尾に環境名をつけておきます。

// 環境プロパティー
public class EnvProperties {
    /** 一時作業ルート・パス */
    public static String TEMP_ROOT_PATH;
    /** ほげルート・パス */
    public static String HOGE_ROOT_PATH;
}

上記クラスの対応するフィールドに環境によって適切な値が格納されます。これを格納するのは下記の環境プロパティー・ローダーです。フィールドに格納しているので、getString(<キー>) とか冗長なことをする必要はありません。フィールドが final じゃないのはあれですけど。

// 環境プロパティー・ローダー
public class EnvPropertiesLoader {
    @InitMethod
    public void init() {
        ResourceBundle bundle = ResourceBundle.getBundle("env");
        String envValue = Env.getValue();
        for (Field field : EnvProperties.class.getFields()) {
            String name = field.getName();
            String value = bundle.getString(name + "." + envValue);
            FieldUtil.set(field, null, value);
        }
    }
}
<!-- 環境プロパティー・ローダー -->
<component class="hoge.framework.internal.EnvPropertiesLoader"/>

更に EnvProperties クラスにフィールドを作るのが面倒なのと、手で作ると間違いの元なので、こんな Excel ファイルを作って EnvProperties クラスも env.properties もマクロで自動生成するようにしました。

プロパティー 結合テスト 統合テスト 本番
TEMP_ROOT_PATH c:/temp /tmp /tmp
HOGE_ROOT_PATH c:/temp /usr/hoge /usr/hoge


なぜ、コンストラクタ・インジェクションじゃなく、@InitMethod にしたかは忘れてしまいました。再ロードかな。。。 ちなみ最初のエントリーの JSTLConstantsRegister によりこの値は JSP からも普通にアクセスすることができます。