らくさん

2008-11-26

Tapestry 5.0.17(RC2) 出た

It contains a change to URL generation (to avoid occasionally empty href's in links). It also adds localizations for Japanese and Greek, and some documentation fixes.

[ANNOUNCE] Tapestry 5.0.17

「localizations for Japanese」とは、バリデーションメッセージといくつかのコンポーネントリソースの日本語化のことです。私が送っておきました。

2008-11-17

コンテキストパラメータのエンコード方法、SEO的には以前の方が良かった?

コンテキストパラメータのエンコード方法が変わるようです - らくさん で書いたように、5.0.16からコンテキストパラメータエンコード方法がかわりましたが、新しい方法だと日本語の文字は $xxxx にエンコードされるので検索エンジンキーワードとして認識してくれないよね、たぶん。以前のエンコード方式は、標準的なURLエンコードをベースにスラッシュなどの扱いだけカスタマイズしたものだったから、それならキーワードとして認識されてたはず。標準的なURLエンコードだとサーブレットコンテナによっては問題があったから変更したみたいなんですが。


あーあと、ブラウザURL欄に直接入力できなくなったのも微妙に残念かも。


新しいコンテキストパラメータエンコーダIoCサービスとして実装されたから差し替えることはできるはずなんで、差し替えてみるかなぁ。


やってみた (2008-11-19追記)

org.apache.tapestry5.services.URLEncoder インターフェースを実装したクラス MyURLEncoderImpl を作って、AppModule.java内で AliasOverrides の設定をする。MyURLEncoderImpl の実装は org.apache.tapestry5.internal.services.URLEncoderImpl をベースにしました。


フレームワークのソースに全く手を入れずにこんなものまで差し替えできるのが、Tapestryの凄いところのひとつです。


AppModule.java:

    public static void contributeAliasOverrides(
            Configuration<AliasContribution> configuration) {

        configuration.add(AliasContribution.create(URLEncoder.class, new MyURLEncoderImpl()));
    }

MyURLEncoderImpl.java:

import java.io.UnsupportedEncodingException;
import java.util.BitSet;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.net.URLCodec;
import org.apache.tapestry5.ioc.internal.util.Defense;
import org.apache.tapestry5.services.URLEncoder;

public class MyURLEncoderImpl implements URLEncoder {

    static final String ENCODED_NULL = "$N";
    static final String ENCODED_BLANK = "$B";

    /**
     * Bit set indicating which character are safe to pass through (when encoding or decoding) as-is.
     */
    private final BitSet safe = new BitSet(128);

    private final URLCodec percentDecoder = new URLCodec();

    {
        markSafe("abcdefghijklmnopqrstuvwxyz");
        markSafe("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
        markSafe("01234567890-_.:");
    }

    private void markSafe(String s) {
        for (char ch : s.toCharArray()) {
            safe.set((int) ch);
        }
    }

    public String encode(String input) {
        if (input == null)
            return ENCODED_NULL;

        if (input.equals(""))
            return ENCODED_BLANK;

        byte[] bytes;
        try {
            bytes = input.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }

        boolean dirty = false;

        boolean percentEncoded = false;

        int length = bytes.length;

        StringBuilder output = new StringBuilder(length * 3);

        for (int i = 0; i < length; i++) {
            int b = bytes[i] & 0xff;

            switch (b) {
                case '$':
                    output.append("$$");
                    dirty = true;
                    continue;
                case '/':
                    output.append("$S");
                    dirty = true;
                    continue;
            }

            if (safe.get(b)) {
                output.append((char) b);
                continue;
            }

            output.append(String.format("%%%02x", b));
            percentEncoded = true;
        }

        return percentEncoded ? output.insert(0, "%09").toString() : dirty ? output.toString() : input;
    }

    public String decode(String input) {
        Defense.notNull(input, "input");

        if (input.equals(ENCODED_NULL))
            return null;

        if (input.equals(ENCODED_BLANK))
            return "";

        boolean percentEncoded = input.startsWith("%09");

        if (percentEncoded) {
            input = input.substring(3);
        } else if (input.charAt(0) == 0x09) {
            input = input.substring(1);
        }

        boolean dirty = false;

        int length = input.length();

        StringBuilder output = new StringBuilder(length);

        for (int i = 0; i < length; i++) {
            char ch = input.charAt(i);

            if (ch == '$') {
                dirty = true;

                if (i + 1 < length) {
                    switch (input.charAt(i + 1)) {
                        case '$':
                            output.append('$');
                            i++;
                            dirty = true;
                            continue;
                        case 'S':
                            output.append('/');
                            i++;
                            dirty = true;
                            continue;
                    }
                }

                throw new IllegalArgumentException(String.format(
                        "Input string '%s' is not valid; the '$' character at position %d should be followed by another '$' or 'S'.",
                        input, i + 1));
            }

            output.append(ch);
        }

        try {
            return percentEncoded ? percentDecoder.decode(output.toString()) : dirty ? output.toString() : input;
        } catch (DecoderException e) {
            throw new IllegalArgumentException("Input string '%s' is not valid.", e);
        }
    }
}

パラメータの先頭に %09 を埋め込んでるのは、Formコンポーネントを使ったときの t:ac パラメータにも対応するため。%09 がデコード済みであればパス経由で渡されたコンテキストパラメータで、デコードされてなければフォーム経由で渡されたと判定してます。あまり奇麗な対応方法ではないけど、奇麗に対応しようとすると結構大変なので。

2008-10-27

コンテキストパラメータのエンコード方法が変わるようです

Tapestry-5.0.16からコンテキストパラメータエンコード方法が変わるようです。

I'm actively working on the issue with URL encoding/decoding. Because
of the differences between Jetty and Tomcat, I'm abandoning standard
URL encoding (i.e., %2f and all that) and using my own scheme.

As a side effect, we can now properly support null and blank string
values in page activation and event contexts (they will appear in URLs
as $N and $B, respectively).

--
Howard M. Lewis Ship

http://mail-archives.apache.org/mod_mbox/tapestry-users/200810.mbox/%3cecd0e3310810241001l28df465aob0d1cf99df3132f8@mail.gmail.com%3e

http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/URLEncoderImpl.java?view=markup&pathrev=707887

を見ると、[a-zA-Z0-9_.:-] 以外は $xxxx (xxxxはJavaの内部文字コード=UTF16) にエンコードされるようです。あと、nullと空文字が使えるようになるのはちょっと便利になるかも。


この変更によって、以前に私が書いた、コンテキストパラメータの日本語対応(というかUTF8対応)といくつかのバグ修正を行ったパッチの該当箇所は全面的に書き換えられたようなので、svnからtrunkをチェックアウトして日本語が通るかどうか確認してみました。テストコードには私が書いたテストがそのまま残っていて、そのテストは全部クリアしていました。ですので、たぶん大丈夫だとは思いますが、Tapestry5を使っている人は5.0.16を早めに試して確認しておいた方がよいかもしれません。5.0.16はRCになるはずなので、不具合がもし残ってたら早く報告しないとそのままリリースされてしまうかもしれませんので。


あと、すでに運用してる場合はパーマリンクが変わっちゃうので5.0.16に更新しづらいかもしれませんね。

2008-09-26

そろそろTapestryについても

何か言っておきたいんだけど、最近全然Tapestryいじってないorz。Tapestry5使ってるサイトは、最近機能追加とかしてないしバグも出ないからかなり放置状態。5.0.12でパッケージ名が変わった(org.apache.tapestry -> org.apache.tapestry5)ところまではわかってる。


dev@tapestry.apache.orgによると、先日リリースされた5.0.15がRCになる予定だったけど幾つかのバグのせいでRCとはならずに、次の5.0.16がRCで10月上旬の予定だそうだ。


遅くとも年内には正式リリースされるかな。

2008-04-26

Formコンポーネントで clientValidation="false" にしたとき、余分なマークアップも消す方法

追記: ソース修正しました。BaseValidationDecoratorは中身からっぽで何もしないクラスなので毎回newするのは無駄でした。


Formコンポーネントで clientValidation="false" にすると、ブラウザ上での入力値検証が行われなくなりますが、出力されるHTML上にはエラー表示用のマークアップが残ったままになってしまいます。style="display:none;" となっているので問題無いのですが、この余分なマークアップも消すためのMixinです。

public class ValidationDecoratorDisabled {

    private static final ValidationDecorator
            DECORATOR_INSTANCE = new BaseValidationDecorator();

    @Inject
    private Environment _environment;

    void setupRender() {
        _environment.push(ValidationDecorator.class, DECORATOR_INSTANCE);
    }

    void cleanupRender() {
        _environment.pop(ValidationDecorator.class);
    }
}

次のように、FormコンポーネントにMixinします。

<t:form t:clientValidation="false" t:mixins="ValidationDecoratorDisabled">
    <t:textfield t:id="query"/>
    <input type="submit" value="検索"/>
</t:form>