Hatena::ブログ(Diary)

cleverwareの日記 このページをアンテナに追加 RSSフィード

2009-11-11

Tagは属性を自分で変更してはいけない(例えばS2SubmitTag)

SAStrutsのSubmitTagは以下のように実装されている

    public int doEndTag() throws JspException {
        if (clientValidate) {
            FormTag formTag = (FormTag) findAncestorWithClass(this,
                    FormTag.class);
            if (formTag == null) {
                throw new JspTagException("FormTag not found.");
            }
            String actionFormName = formTag.getBeanName();
            StringBuilder sb = new StringBuilder();
            sb.append("var myForm = document.forms['").append(actionFormName)
                    .append("'];");
            sb.append("myForm.id='").append(actionFormName).append("_").append(
                    property).append("'; ");
            sb.append("return validate").append(
                    actionFormName.substring(0, 1).toUpperCase()).append(
                    actionFormName.substring(1)).append("_").append(property)
                    .append("(myForm);");
            String originalOnclick = getOnclick();
            if (originalOnclick == null
                    || originalOnclick.startsWith("var myForm")) {
                setOnclick(sb.toString());
            } else {
                setOnclick(originalOnclick + ";" + sb.toString());
            }
        }
        return super.doEndTag();
    }

setOnClick()で属性を書き換えて、super.doEndTag()の出力を書き換えているわけだが、こうするとJSPのTagキャッシュで問題が生じる。

たとえば、

<s:submit property="update" value="更新" onclick="hoge=1" clientValidate="true"/>

だった場合、これらの『指定された属性を含めたキーにして』JSPはタグをキャッシュに保存し、同じ属性が指定された場合にそのTagのインスタンスを再利用する。同じ属性が指定されている前提なので、当然『Tagのrelease()が呼ばれて再度属性が設定されるということはない』。つまり一度設定された属性はキャッシュに置かれたTagで使い回される。

このため、Tagの中で属性を書き換えるとおかしなことになってしまう。

上記の場合だと、1回目は期待通り onClick="hoge=1;var myForm = ....._update(myForm)" と出力されるが、

2回目は onClick="hoge=1;var myForm = .....;var myForm = ......;" となってしまう。つまり、属性を書き換えたために、その書き換えた内容に追加していく動作になってしまうわけだ。

これを回避するためには、タグの属性について不変であるようにするため、

    public int doEndTag() throws JspException {
        String originalOnclick = getOnclick();
        ....
            if (originalOnclick == null) {
                setOnclick(sb.toString());
            } else {
                setOnclick(originalOnclick + ";" + sb.toString());
            }
        }
        int ret = super.doEndTag();
        setOnClick(originalOnClick);
        return ret;
    }

こんな感じにしないといけない。

originalOnclick.startsWith("var myForm") の検査は冗長なので不要になる。

2009-07-16

GenericUtil#getActualClass() で null チェックが抜けているような

GenericUtil#getActualClass() の中で GenericArrayType だった場合、さらにコンポーネントタイプについて getActualClass() を呼び出しているけど、コンポーネントタイプが PrameterizedType TypeVariableでパラメータが解決出来ない場合nullとなるので、返り値がnullかどうかチェックしていないのはまずいと思う。

例えば SAStruts で Action に以下のようなClassを持つプロパティがあると、そいつを(String.class)をBeanWrap()しようとし、(巡り巡って…長くなるので省略)上記の条件に該当してヌルポ。

public Class<?> hoge = String.class;
/* もしくは */
public Class<?> getAnyClass() {
  return String.class;
}
java.lang.NullPointerException
	java.lang.reflect.Array.newArray(Native Method)
	java.lang.reflect.Array.newInstance(Array.java:52)
	org.seasar.framework.util.tiger.GenericUtil.getActualClass(GenericUtil.java:367)
	org.seasar.framework.beans.factory.ParameterizedClassDescFactoryProvider.createParameterizedClassDesc(ParameterizedClassDescFactoryProvider.java:73)
	org.seasar.framework.beans.factory.ParameterizedClassDescFactoryProvider.createParameterizedClassDesc(ParameterizedClassDescFactoryProvider.java:59)
	org.seasar.framework.beans.factory.ParameterizedClassDescFactory.createParameterizedClassDesc(ParameterizedClassDescFactory.java:122)
	org.seasar.framework.beans.impl.PropertyDescImpl.setUpParameterizedClassDesc(PropertyDescImpl.java:159)
	org.seasar.framework.beans.impl.PropertyDescImpl.<init>(PropertyDescImpl.java:120)
	org.seasar.framework.beans.impl.BeanDescImpl.setupReadMethod(BeanDescImpl.java:602)
	org.seasar.framework.beans.impl.BeanDescImpl.setupPropertyDescs(BeanDescImpl.java:542)
	org.seasar.framework.beans.impl.BeanDescImpl.<init>(BeanDescImpl.java:113)
	org.seasar.framework.beans.factory.BeanDescFactory.getBeanDesc(BeanDescFactory.java:60)
	org.seasar.struts.action.BeanWrapper.<init>(BeanWrapper.java:54)
	org.seasar.struts.action.WrapperUtil.convert(WrapperUtil.java:65)
	org.seasar.struts.action.S2RequestProcessor.exportPropertiesToRequest(S2RequestProcessor.java:346)
	org.seasar.struts.action.S2RequestProcessor.doForward(S2RequestProcessor.java:304)
	org.apache.struts.action.RequestProcessor.processForwardConfig(RequestProcessor.java:398)
	org.seasar.struts.action.S2RequestProcessor.process(S2RequestProcessor.java:129)
	org.apache.struts.action.ActionServlet.process(ActionServlet.java:1196)
	org.apache.struts.action.ActionServlet.doGet(ActionServlet.java:414)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:617)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
	org.seasar.framework.container.filter.S2ContainerFilter.doFilter(S2ContainerFilter.java:79)
	org.seasar.framework.container.hotdeploy.HotdeployFilter.doHotdeployFilter(HotdeployFilter.java:86)
	org.seasar.framework.container.hotdeploy.HotdeployFilter.doFilter(HotdeployFilter.java:67)
	org.seasar.struts.filter.RoutingFilter.forward(RoutingFilter.java:219)
	org.seasar.struts.filter.RoutingFilter.doFilter(RoutingFilter.java:90)
	org.seasar.framework.container.filter.S2ContainerFilter.doFilter(S2ContainerFilter.java:79)
	org.seasar.framework.container.hotdeploy.HotdeployFilter.doHotdeployFilter(HotdeployFilter.java:99)
	org.seasar.framework.container.hotdeploy.HotdeployFilter.doFilter(HotdeployFilter.java:67)
	org.seasar.extension.filter.EncodingFilter.doFilter(EncodingFilter.java:69)

2009-07-02

echoping で submission ポートを監視できるようにする

echoping には SMTP プロトコルでの応答があるか確認する便利な機能があるのだが、ポートを指定することが出来ない。

$ echoping -v -S xxx.xxx.xxx.xxx

This is echoping, version 6.0.2.

Trying to connect to internet address xxx.xxx.xxx.xxx 25 to transmit 6 bytes...
Trying to send 256 bytes to internet address xxx.xxx.xxx.xxx...
Connected...
TCP Latency: 0.000243 seconds
Sent (6 bytes)...
Application Latency: 0.011435 seconds
26 bytes read from server.
Estimated TCP RTT: 0.0024 seconds (std. deviation 0.003)
Elapsed time: 3.438002 seconds
$ echoping -v -S xxx.xxx.xxx.xxx:587

This is echoping, version 6.0.2.

echoping: The syntax hostname:port is only for HTTP or ICP
$

現実のシステム管理だと 25番以外のポートで運用することも多く不便なので、ちょっとした改造を行った。

--- echoping-6.0.2/echoping.c.~1~       2007-04-05 21:40:49.000000000 +0900
+++ echoping-6.0.2/echoping.c   2009-07-02 14:07:10.000000000 +0900
@@ -560,7 +560,7 @@
                err_quit("Cannot convert %s to UTF-8 encoding: wrong locale (%s)?",
                         server, stringprep_locale_charset());
 #endif
-       if (!http && !icp) {
+       if (!http && !icp && !smtp) {
                for (p = server; *p && (*p != ':'); p++) {
                }
                if (*p && (*p == ':')) {
@@ -636,6 +636,44 @@
                }
        }
 #endif
+#ifdef SMTP
+       if (smtp) {
+               char           *text_port = NULL;
+
+               if (*server == '[') {   /* RFC 2732 */
+                       server++;
+                       for (p = server; *p && *p != ']'; p++) {
+                       }
+                       p++;
+                       if (*p == ':') {
+                               p--;
+                               *p = 0;
+                               text_port = p + 2;
+                               strncpy(port_name, text_port, NI_MAXSERV);
+                       } else {
+                               p--;
+                               if (*p == ']') {
+                                       *p = 0;
+                               } else {
+                                       (void) fprintf(stderr,
+                                                      "%s: Invalid syntax for IPv6 address.\n",
+                                                      progname);
+                                       exit(1);
+                               }
+                       }
+               } else {
+                       for (p = server; *p; p++) {
+                               if (*p == ':') {
+                                       *p = 0;
+                                       text_port = p + 1;
+                                       if (strcmp(text_port, ""))
+                                               strncpy(port_name, text_port,
+                                                       NI_MAXSERV);
+                               }
+                       }
+               }
+       }
+#endif

 #ifdef LIBIDN
        /*

こんな感じで使えます

% echoping -v -S xxx.xxx.xxx.xxx:587

This is echoping, version 6.0.2.

Trying to connect to internet address xxx.xxx.xxx.xxx 587 to transmit 6 bytes...
Connected...
TCP Latency: 0.022983 seconds
Sent (6 bytes)...
Application Latency: 0.023221 seconds
15 bytes read from server.
Estimated TCP RTT: 0.0230 seconds (std. deviation 0.017)
Elapsed time: 0.070736 seconds
%

2009-06-15

プラグインの検索はどのように行われるか(There is no more plugin-registry.xml)

Hudoson で突然ビルドが通らなくなった。versionを指定しないと「プラグインが見つからない」とのたまわれる。こんな感じのありきたりの記述:

    <extensions>
      <extension>
        <groupId>org.apache.maven.wagon</groupId>
        <artifactId>wagon-webdav</artifactId>
      </extension>
    </extensions>

少しググると、同様の問題の記事はあるものの明確な解決策は version を書くことしか見あたらない。確かに version を書けばビルドは出来るようになります。しかし、ついさっきまで version 無しで動いていたものが動かないというのはどうも気持ちが悪い。

で、いろいろ調べて判明した事
  1. ビルド出来なくなる直前に Nexus で構築したリポジトリーミラーを参照するように ~/.m2/settings.xmlを変更した。
  2. ミラーを参照しないように戻しても依然ビルド不能。
  3. Maven - Introduction to the Plugin Registry には~/.m2/plugin-registry.xml の記述で、バージョンの解決を行っていると書かれている。このファイルの内容が、ミラーを参照したときに書き換わったのか?
  4. しかしplugin-registry.xmlなんてどこにもない。ミラーを使わないようにしても、挙動が元に戻らないのだから、どこかにversionを指定しない場合のプラグインバージョンを指定する情報が残っているはず。
  5. ちなみに、mavenorg.apache.maven.wagon:wagon-webdav:jar:RELEASEを取得しようとしている。RELEASEは、レポジトリメタファイルにRELEASEという名前でタグ付けされたバージョンがあればそれを使うということらしい。
結論
  1. mvn -Uと指定するとリモートリポジトリインデックスを検索して最新のプラグインを使用するようになる。これは1回だけ行えばよく、検索の結果はローカルリポジトリ内に記録される。突然ビルド出来なくなったのはNexusによるミラーを参照したためにプラグインが検索できなかったためで、これは別件。
  2. plugin-registry.xmlはもう現在のmavenでは作られない。リモートリポジトリからプラグインのバージョンを検索した結果(mvn -Uの結果)は<ローカルリポジトリ>/org/apache/maven/plugins/<プラグイン>ディレクトリの中にmaven-metadata-*.xml(*は多分リモートリポジトリのid)というファイルとしておかれる。*1

repository/org/apache/maven/wagon/wagon-webdav/maven-metadata-central.xmlの例:

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>org.apache.maven.wagon</groupId>
  <artifactId>wagon-webdav</artifactId>
  <versioning>
    <latest>1.0-beta-2</latest>
    <release>1.0-beta-2</release>
    <versions>
      <version>1.0-beta-1</version>
      <version>1.0-beta-2</version>
    </versions>
    <lastUpdated>20090325190912</lastUpdated>
  </versioning>
</metadata>

2009-06-04

CollectionsのListは時には過剰だ、シンプルに考えよう

ふと目にとまったので、ソースを読んだが突っ込みどころが多いような ;-)

d:id:nowokay:20090424 過去の状態のスナップショットを取れるMap

とりあえずシンプルにした方が、速度もメモリ消費もよくなるかな。ArrayListを使うのはメモリ的に無駄が多いし、配列の初期サイズで収まらなくなったときのことを考えれば、

あと、データを変更せず追記する一方なので、ストレージの仕組みとして使うと、変更回数の制限があるSSDと相性がいいとか。

という売り文句にはそぐわないと思う。

履歴は単方向リストで十分だし、常に先頭が最新の状態の方がコードがシンプルになります。要するに標準のライブラリは万能ではないし、使わずに済ませた方がシンプルな場合もあるということ。

こんな感じ:

public class VersionedMap<K, V> implements Map<K, V> {

    static class Version<V> {
        Version<V> next;
        long time = new Date().getTime();
        V data;
        boolean exist;

        Version(Version<V> next, V data) {
            this.next = next;
            this.data = data;
            this.exist = true;
        }

        Version(Version<V> next) {
            this.next = next;
            this.exist = false;
        }
    }

    protected Map<K, Version<V>> repos = new HashMap<K, Version<V>>();
    private int currentCount = 0;

    public VersionedMap() {
    }

    @Override
    public V get(Object key) {
        Version<V> current = repos.get(key);
        return current != null && current.exist ? current.data : null;
    }

    @Override
    public V put(K key, V value) {
        Version<V> old = repos.put(key, new Version<V>(repos.get(key), value));
        if (old != null && old.exist) {
            return old.data;
        }
        currentCount++;
        return null;
    }

    @SuppressWarnings("unchecked")
    @Override
    public V remove(Object key) {
        if (repos.containsKey(key)) {
            Version<V> old = repos.put((K)key, new Version<V>(repos.get(key)));
            if (old.exist) {
                currentCount--;
                return old.data;
            }
        }
        return null;
    }

    @Override
    public Set<java.util.Map.Entry<K, V>> entrySet() {
        Set<Map.Entry<K, V>> ret = new HashSet<Map.Entry<K, V>>();
        for (Map.Entry<K, Version<V>> e : repos.entrySet()) {
            Version<V> head = e.getValue();
            if (head.exist)
                ret.add(new AbstractMap.SimpleEntry<K, V>(e.getKey(),
                        head.data));
        }
        return ret;
    }

    public Map<K, V> getSnapShot(Date d) {
        Map<K, V> ret = new HashMap<K, V>();
        long time = d.getTime();
        for (Map.Entry<K, Version<V>> e : repos.entrySet()) {
            Version<V> history = e.getValue();
            assert history != null;
            while (history != null) {
                if (history.time < time)
                    break;
                history = history.next;
            }
            if (history != null && history.exist) {
                ret.put(e.getKey(), history.data);
            }
        }
        return ret;
    }

    ...以下省略...
}