第5章: 基本コンポーネント

Wicketアプリケーションは、コンポーネントの組み合わせで構成されます。 この章では、Label、Link、WebMarkupContainerなど、あらゆるWicketアプリの土台となる基本コンポーネントを学びます。 それぞれのコンポーネントについて、Java側のコードとHTML側のマークアップの両方を示しながら解説します。

この章で紹介するすべてのコンポーネントは org.apache.wicket パッケージに属しています。 Wicket 10.x では Jakarta 名前空間を使用するため、import jakarta.servlet.* 系のインポートが必要になる場合があります。

Label

Label は Wicket で最も基本的なコンポーネントです。 HTML タグの中にテキストを挿入するために使用します。 Swing の JLabel に相当するもので、ほとんどすべての Wicket ページで使われます。

基本的な使い方

もっともシンプルな使い方は、コンストラクタに wicket:id とテキスト文字列を渡す方法です。 HTML テンプレート内のプレースホルダーテキストは、Java 側で指定した値に置き換えられます。

HomePage.java
public class HomePage extends WebPage {
    public HomePage() {
        // "msg" は wicket:id に対応する
        add(new Label("msg", "こんにちは、Wicket!"));
    }
}
HomePage.html
<html xmlns:wicket="http://wicket.apache.org">
<body>
  <!-- placeholder は実行時に置換される -->
  <span wicket:id="msg">ここにテキスト</span>
</body>
</html>

レンダリング結果は以下のようになります:

<span>こんにちは、Wicket!</span>

wicket:id 属性は、レンダリング時に自動的に除去されます。 そのため、最終的にブラウザに出力される HTML にはWicket 固有の属性は含まれません。 これはデザイナーとの協業を容易にする Wicket の設計思想の一つです。

Modelとの併用

固定文字列だけでなく、Model オブジェクトを使って動的な値をバインドできます。 Model は Wicket のデータバインディングの中核であり、第11章で詳しく解説しますが、 ここでは基本的な使い方を紹介します。

HomePage.java
public class HomePage extends WebPage {
    private String userName = "田中太郎";

    public HomePage() {
        // Model.of() で変数をラップする
        add(new Label("name",
            Model.of(userName)));

        // PropertyModel を使うとフィールドに直接バインド
        add(new Label("name2",
            new PropertyModel<>(this, "userName")));
    }
}
HomePage.html
<p>ようこそ、
  <span wicket:id="name">名前</span>
  さん!
</p>

<p>PropertyModel版:
  <span wicket:id="name2">名前</span>
</p>

Model.of(someVariable) は変数のその時点の値のスナップショットを保持します。 変数の値が後から変わっても Label の表示は更新されません。 動的に追従させたい場合は PropertyModelLambdaModel を使用してください。

ラムダ式での利用

Wicket 10 では、Java のラムダ式を活用した LambdaModel がよく使われます。 PropertyModel と異なり、型安全でリファクタリングにも強い記述が可能です。

HomePage.java
public class HomePage extends WebPage {
    private String userName = "田中太郎";

    public HomePage() {
        // LambdaModel でメソッド参照を使う
        add(new Label("name",
            LambdaModel.of(this::getUserName)));

        // 現在時刻を表示する例
        add(new Label("time",
            LambdaModel.of(() ->
                LocalDateTime.now()
                    .format(DateTimeFormatter
                        .ofPattern("yyyy/MM/dd HH:mm:ss"))
            )));
    }

    public String getUserName() {
        return userName;
    }
}
HomePage.html
<p>ユーザー名:
  <span wicket:id="name">名前</span>
</p>

<p>現在時刻:
  <span wicket:id="time">00:00:00</span>
</p>

LambdaModel.of(() -> ...) は、ページがレンダリングされるたびにラムダ式が評価されます。 そのため、現在時刻のような動的な値を表示するのに最適です。 AJAX でのページ部分更新時にも、最新の値が取得されます。

HTMLエスケープ

Label は、デフォルトで出力テキストをHTMLエスケープします。 これはクロスサイトスクリプティング(XSS)攻撃を防ぐための重要なセキュリティ機能です。

XSS攻撃の防止例
// ユーザーが入力した悪意のある文字列
String userInput = "<script>alert('XSS')</script>";

// Label は自動的にエスケープする
add(new Label("output", userInput));

// レンダリング結果:
// &lt;script&gt;alert('XSS')&lt;/script&gt;
// → ブラウザにはそのままテキストとして表示される

もし HTML をそのまま出力したい場合は、setEscapeModelStrings(false) を呼び出します。 ただし、ユーザー入力を含む場合は絶対にエスケープを無効にしないでください。

HTMLエスケープを無効にする(信頼できるHTMLのみ)
// 管理者が作成した安全なHTMLコンテンツの場合のみ
Label htmlLabel = new Label("content",
    "<strong>太字</strong>のテキスト");
htmlLabel.setEscapeModelStrings(false);
add(htmlLabel);

setEscapeModelStrings(false) を使用する場合は、出力するHTMLが信頼できるソースからのものであることを必ず確認してください。 ユーザーが入力した値をエスケープなしで出力すると、XSS脆弱性の原因になります。

MultiLineLabel

MultiLineLabel は、改行文字(\n)を含むテキストを表示するためのコンポーネントです。 通常の Label では改行文字はそのままスペースとして表示されますが、 MultiLineLabel は改行を <br/> タグに変換して出力します。

MessagePage.java
public class MessagePage extends WebPage {
    public MessagePage() {
        String message =
            "1行目のテキスト\n"
            + "2行目のテキスト\n"
            + "3行目のテキスト";

        add(new MultiLineLabel("message",
            message));
    }
}
MessagePage.html
<html xmlns:wicket="http://wicket.apache.org">
<body>
  <p wicket:id="message">
    プレースホルダー
  </p>
</body>
</html>

レンダリング結果:

<p>1行目のテキスト<br/>2行目のテキスト<br/>3行目のテキスト</p>

MultiLineLabel もHTMLエスケープが有効です。 改行以外の HTML タグ(例: <b>)はエスケープされてそのままテキストとして表示されます。 データベースやテキストエリアから取得した複数行テキストを安全に表示するのに最適なコンポーネントです。

Link は、クリック時にサーバー側の処理を実行するためのコンポーネントです。 HTML の <a> タグに対応し、onClick() メソッドをオーバーライドしてクリック時の動作を定義します。 Spring MVC のコントローラメソッドに相当する処理を、コンポーネントのメソッドとして記述できるのが Wicket の特徴です。

HomePage.java
public class HomePage extends WebPage {
    private int counter = 0;

    public HomePage() {
        final Label countLabel =
            new Label("count",
                LambdaModel.of(this::getCounter));
        add(countLabel);

        add(new Link<Void>("countUp") {
            @Override
            public void onClick() {
                counter++;
                // ページが再レンダリングされ、
                // countLabel に最新値が反映される
            }
        });
    }

    public int getCounter() {
        return counter;
    }
}
HomePage.html
<html xmlns:wicket="http://wicket.apache.org">
<body>
  <p>カウンター:
    <span wicket:id="count">0</span>
  </p>

  <a wicket:id="countUp">
    カウントアップ
  </a>
</body>
</html>

Link<Void> のジェネリクス型パラメータはモデルオブジェクトの型を示します。 モデルを使わない場合は Void を指定するのが慣例です。

匿名クラスが冗長に感じる場合は、Link.onClick(SerializableConsumer) のようなファクトリメソッドやラムダ対応のAPIを活用する方法もあります。 ただし、Wicket 10 の標準 Link は匿名クラスでのオーバーライドが基本パターンです。 ページ遷移だけの場合は、次に紹介する BookmarkablePageLink を使うのが簡潔です。

ページ遷移を行うLinkの例
add(new Link<Void>("goToAbout") {
    @Override
    public void onClick() {
        // 別のページへ遷移する
        setResponsePage(new AboutPage());
    }
});

Link のクリックで生成される URL はステートフルなもので、セッションに紐づきます。 そのため、ブックマークには適しません。 ブックマーク可能なリンクが必要な場合は BookmarkablePageLink を使用してください。

ExternalLink は、外部 URL へのリンクを生成するためのコンポーネントです。 通常の Link がサーバー側の処理を実行するのに対し、 ExternalLink は単純に指定した URL への <a href="..."> タグを生成します。

LinksPage.java
public class LinksPage extends WebPage {
    public LinksPage() {
        // 第2引数: URL, 第3引数: リンクテキスト
        add(new ExternalLink("google",
            "https://www.google.com",
            "Google検索"));

        // リンクテキストをHTML側で指定する場合
        // 第3引数を省略する
        add(new ExternalLink("wicket",
            "https://wicket.apache.org"));

        // Model を使って動的にURLを指定
        add(new ExternalLink("dynamic",
            Model.of(buildUrl()),
            Model.of("動的リンク")));
    }

    private String buildUrl() {
        return "https://example.com/search?q=wicket";
    }
}
LinksPage.html
<!-- Java側でリンクテキストを指定 -->
<a wicket:id="google">
  placeholder
</a>

<!-- HTML側でリンクテキストを指定 -->
<a wicket:id="wicket">
  Apache Wicket 公式サイト
</a>

<!-- 動的URLのリンク -->
<a wicket:id="dynamic">
  placeholder
</a>

レンダリング結果の例:

<a href="https://www.google.com">Google検索</a>
<a href="https://wicket.apache.org">Apache Wicket 公式サイト</a>

ExternalLinktarget="_blank" などの属性も通常の HTML のように記述できます。 <a wicket:id="google" target="_blank"> とすれば、新しいタブで開くリンクになります。 Wicket は wicket:id 以外の既存の HTML 属性をそのまま保持します。

BookmarkablePageLink は、Wicket アプリケーション内の別のページへのブックマーク可能なリンクを生成します。 セッションに依存しない URL を生成するため、ユーザーがブックマークしたり、URL を共有したりできます。

HomePage.java
public class HomePage extends WebPage {
    public HomePage() {
        // 基本的なBookmarkablePageLink
        add(new BookmarkablePageLink<>(
            "aboutLink", AboutPage.class));

        // パラメータ付きのリンク
        PageParameters params = new PageParameters();
        params.add("id", 42);
        params.add("action", "view");

        add(new BookmarkablePageLink<>(
            "detailLink",
            DetailPage.class,
            params));
    }
}
HomePage.html
<!-- 基本リンク -->
<a wicket:id="aboutLink">
  このサイトについて
</a>

<!-- パラメータ付きリンク -->
<a wicket:id="detailLink">
  詳細ページへ
</a>

<!-- レンダリング結果の例:
  <a href="./wicket/bookmarkable/
    com.example.DetailPage?id=42&action=view">
    詳細ページへ
  </a>

  マウントURL設定済みの場合:
  <a href="./detail/42/view">
    詳細ページへ
  </a>
-->

setResponsePageとの違い

Wicket でのページ遷移には、setResponsePage()BookmarkablePageLink の2つの方法があります。 それぞれの違いを理解することは重要です。

特徴 setResponsePage(new Page()) BookmarkablePageLink
URL セッション依存のステートフルURL ブックマーク可能なステートレスURL
パラメータ コンストラクタ引数で自由に渡せる PageParameters(文字列のみ)
ブックマーク 不可(セッション切れでエラー) 可能
使用場面 フォーム送信後の遷移、複雑なオブジェクト渡し ナビゲーション、外部共有可能なURL
サーバー処理 Link の onClick() 内で呼び出す クリック時にサーバー処理なし

一般的なガイドラインとして、ナビゲーションメニューや一覧ページからの詳細リンクには BookmarkablePageLink を使い、 フォーム送信後のリダイレクトや状態を伴う遷移には setResponsePage() を使うのが適切です。 SEO が重要なページでは、必ず BookmarkablePageLink を使用してください。

WebMarkupContainer

WebMarkupContainer は、HTML の任意の要素に対応する汎用的なコンテナコンポーネントです。 それ自体はテキストやリンクのような具体的な機能を持ちませんが、 表示・非表示の切替、属性の動的追加、子コンポーネントのグルーピングなど、多くの場面で活躍します。

表示・非表示の切替

最もよく使われるパターンは、setVisible() による HTML セクションの表示・非表示の制御です。

DashboardPage.java
public class DashboardPage extends WebPage {
    private boolean isAdmin = false;

    public DashboardPage() {
        // 管理者パネルの表示制御
        WebMarkupContainer adminPanel =
            new WebMarkupContainer("adminPanel");
        adminPanel.setVisible(isAdmin);
        add(adminPanel);

        // adminPanel 内に子コンポーネントを追加
        adminPanel.add(
            new Label("adminMsg",
                "管理者用メッセージ"));

        // エラーメッセージの条件付き表示
        String errorMsg = getErrorMessage();
        WebMarkupContainer errorBox =
            new WebMarkupContainer("errorBox");
        errorBox.setVisible(errorMsg != null);
        add(errorBox);

        errorBox.add(new Label("errorText",
            errorMsg != null ? errorMsg : ""));
    }
}
DashboardPage.html
<!-- isAdmin=true のときだけ表示 -->
<div wicket:id="adminPanel"
     class="admin-section">
  <h2>管理者メニュー</h2>
  <p wicket:id="adminMsg">msg</p>
</div>

<!-- エラーがある時だけ表示 -->
<div wicket:id="errorBox"
     class="error-message">
  <span wicket:id="errorText">
    エラー
  </span>
</div>

setVisible(false) を設定したコンポーネントは、HTML 出力から完全に除外されます。 display: none のような CSS による非表示とは異なり、HTML 自体が出力されないため、 ブラウザの「ソースを表示」でも見えません。これはセキュリティ上も優れた動作です。

属性の動的追加

AttributeModifierAttributeAppender を使って、 HTML 属性を動的に追加・変更できます。

StyledPage.java
public class StyledPage extends WebPage {
    public StyledPage() {
        WebMarkupContainer box =
            new WebMarkupContainer("box");

        // class 属性を追加
        box.add(AttributeModifier.append(
            "class", "highlight"));

        // style 属性を設定
        box.add(AttributeModifier.replace(
            "style",
            "background-color: #ffeb3b;"));

        // title 属性を追加
        box.add(new AttributeAppender(
            "title",
            Model.of("ツールチップ表示")));

        add(box);
    }
}
StyledPage.html
<!-- 元のHTML -->
<div wicket:id="box"
     class="base-style">
  コンテンツ
</div>

<!-- レンダリング結果 -->
<!--
<div class="base-style highlight"
     style="background-color: #ffeb3b;"
     title="ツールチップ表示">
  コンテンツ
</div>
-->

コンポーネントのグルーピング

複数のコンポーネントを論理的にグループ化して、まとめて表示・非表示を制御したい場合にも WebMarkupContainer が便利です。

グルーピングの例
// ログイン状態に応じてグループ全体を切り替え
WebMarkupContainer loggedInSection =
    new WebMarkupContainer("loggedIn");
loggedInSection.setVisible(isAuthenticated());
add(loggedInSection);

// loggedInSection 内にのみ表示されるコンポーネント群
loggedInSection.add(new Label("welcome",
    "ようこそ、" + getUser().getName() + "さん"));
loggedInSection.add(new Link<Void>("logout") {
    @Override
    public void onClick() {
        getSession().invalidate();
        setResponsePage(HomePage.class);
    }
});

WebMarkupContainer<div><span><tr><td> など、 あらゆる HTML 要素にアタッチできます。テーブルの行を動的に表示制御する場合は <tr wicket:id="row">WebMarkupContainer を割り当てると便利です。

Image

Image コンポーネントは、HTML の <img> タグに対応します。 画像のソースを Java 側で指定でき、クラスパス上のリソースや Web アプリケーションフォルダ内のファイルを参照できます。

クラスパスリソース(PackageResourceReference)

Java クラスと同じパッケージに画像ファイルを配置し、PackageResourceReference で参照する方法です。 コンポーネントと画像を同じパッケージにまとめられるため、再利用性が高くなります。

PhotoPage.java
public class PhotoPage extends WebPage {
    public PhotoPage() {
        // クラスパス上の画像を参照
        // PhotoPage.class と同じパッケージ内の
        // logo.png を参照する
        add(new Image("logo",
            new PackageResourceReference(
                PhotoPage.class,
                "logo.png")));

        // 別パッケージの画像も参照可能
        add(new Image("icon",
            new PackageResourceReference(
                CommonResources.class,
                "icons/user.png")));
    }
}
PhotoPage.html
<html xmlns:wicket="http://wicket.apache.org">
<body>
  <!-- src はWicketが自動設定 -->
  <img wicket:id="logo"
       alt="サイトロゴ" />

  <img wicket:id="icon"
       alt="ユーザーアイコン"
       width="32"
       height="32" />
</body>
</html>

ファイル配置のイメージ:

src/main/java/
  com/example/pages/
    PhotoPage.java
    PhotoPage.html
    logo.png          ← クラスパスリソース
  com/example/resources/
    CommonResources.java
    icons/
      user.png        ← 別パッケージのリソース

webappフォルダリソース(ContextRelativeResource)

Webアプリケーションの webapp フォルダ直下に配置された画像を参照する場合は、 ContextRelativeResource を使用します。

TopPage.java
public class TopPage extends WebPage {
    public TopPage() {
        // webapp/images/banner.jpg を参照
        add(new Image("banner",
            new ContextRelativeResource(
                "images/banner.jpg")));
    }
}
TopPage.html
<img wicket:id="banner"
     alt="サイトバナー"
     class="banner-image" />
src/main/webapp/
  images/
    banner.jpg        ← webappフォルダ内のリソース

一般的に、コンポーネント固有の画像は PackageResourceReference で管理し、 サイト全体で共有する画像(バナー、ファビコンなど)は webapp フォルダに配置して ContextRelativeResource で参照するのが推奨パターンです。 PackageResourceReference を使うと、Wicket がキャッシュヘッダや バージョニング付きURLを自動的に生成してくれるというメリットもあります。

ListView(プレビュー)

ListView は、リスト(List)の各要素を繰り返し表示するためのコンポーネントです。 詳細は第7章: リピータで解説しますが、 ここでは基本的な使い方のプレビューを紹介します。

FruitPage.java
public class FruitPage extends WebPage {
    public FruitPage() {
        List<String> fruits = List.of(
            "りんご", "みかん", "バナナ",
            "ぶどう", "いちご");

        add(new ListView<String>(
                "fruitList", fruits) {
            @Override
            protected void populateItem(
                    ListItem<String> item) {
                // 各要素のモデルから値を取得
                String fruit =
                    item.getModelObject();
                item.add(new Label(
                    "name", fruit));
            }
        });
    }
}
FruitPage.html
<html xmlns:wicket="http://wicket.apache.org">
<body>
  <h2>フルーツ一覧</h2>
  <ul>
    <!-- この <li> がリストの
         各要素分繰り返される -->
    <li wicket:id="fruitList">
      <span wicket:id="name">
        フルーツ名
      </span>
    </li>
  </ul>
</body>
</html>

レンダリング結果:

<ul>
  <li><span>りんご</span></li>
  <li><span>みかん</span></li>
  <li><span>バナナ</span></li>
  <li><span>ぶどう</span></li>
  <li><span>いちご</span></li>
</ul>

ListView は小規模なリスト表示に適していますが、大量データの場合は DataViewDataTable の使用を検討してください。 これらのコンポーネントはページング機能を内蔵しており、 パフォーマンスにも配慮した設計になっています。詳しくは第7章をご覧ください。

コンポーネントの共通機能

すべての Wicket コンポーネントは Component クラスを継承しており、 共通のメソッドを利用できます。ここでは、実務で頻繁に使用する重要なメソッドを紹介します。

表示制御: setVisible() / setEnabled()

VisibilityExample.java
public class VisibilityExample extends WebPage {
    public VisibilityExample() {
        Label secret = new Label(
            "secret", "秘密の情報");

        // setVisible(false): HTMLに出力されない
        secret.setVisible(false);
        add(secret);

        Link<Void> disabledLink =
            new Link<Void>("link") {
            @Override
            public void onClick() { /* ... */ }
        };

        // setEnabled(false): 表示はされるが
        // クリックしても反応しない
        disabledLink.setEnabled(false);
        add(disabledLink);
    }
}
VisibilityExample.html
<!-- setVisible(false) → 出力されない -->
<p wicket:id="secret">
  秘密
</p>

<!-- setEnabled(false) →
     リンクとして機能しない -->
<a wicket:id="link">
  無効なリンク
</a>

<!-- レンダリング結果:
  (secret は出力なし)
  <a class="disabled">無効なリンク</a>
  ※href属性が除去される
-->
メソッド 効果 HTML出力
setVisible(false) コンポーネントを非表示にする タグごと出力されない
isVisible() 現在の表示状態を取得 --
setEnabled(false) コンポーネントを無効化する タグは出力されるが機能しない
isEnabled() 現在の有効状態を取得 --

AJAX関連設定

AJAX を使用してコンポーネントを部分更新する場合に必要な設定です。 詳しくは第8章: AJAX対応で解説しますが、 基本設定として覚えておいてください。

AJAX更新に必要な設定
Label counter = new Label("counter",
    LambdaModel.of(this::getCount));

// AJAX で更新するには markupId が必要
// → <span id="counter1"> のような id 属性が付与される
counter.setOutputMarkupId(true);
add(counter);

WebMarkupContainer panel =
    new WebMarkupContainer("panel");

// AJAX で表示・非表示を切り替える場合に必要
// → 非表示時も <div id="panel1" style="display:none"></div>
//   のようなプレースホルダーが出力される
panel.setOutputMarkupPlaceholderTag(true);
panel.setVisible(false);
add(panel);
メソッド 用途 効果
setOutputMarkupId(true) AJAXで内容を更新する HTMLに id 属性を出力する
setOutputMarkupPlaceholderTag(true) AJAXで表示/非表示を切り替える 非表示時も空タグを出力し、後からAJAXで表示可能にする

setVisible(false) のコンポーネントを AJAX で後から表示したい場合、 setOutputMarkupPlaceholderTag(true) を事前に設定しておく必要があります。 これがないと、非表示時に HTML タグ自体が存在しないため、JavaScript から操作できません。 AJAX を使う予定のあるコンポーネントには、最初から設定しておくのが安全です。

レンダリング制御: setRenderBodyOnly(true)

setRenderBodyOnly(true) を設定すると、コンポーネントが割り当てられたタグ自体は出力されず、 その子要素(ボディ)のみが出力されます。

RenderExample.java
Label name = new Label("name", "田中");

// タグ(<span>)を出力しない
name.setRenderBodyOnly(true);
add(name);
結果の比較
<!-- HTML テンプレート -->
<p>名前: <span wicket:id="name">X</span></p>

<!-- 通常のレンダリング結果 -->
<p>名前: <span>田中</span></p>

<!-- setRenderBodyOnly(true) の場合 -->
<p>名前: 田中</p>

setRenderBodyOnly(true) を設定したコンポーネントには setOutputMarkupId(true) を同時に使えません。 タグが出力されないため、id 属性を付与する場所がないからです。 AJAX で更新する予定のコンポーネントには setRenderBodyOnly(true) を使用しないでください。

Behaviorの追加: add(Behavior)

Behavior は、コンポーネントにプラグインのように機能を追加する仕組みです。 詳細は第9章: Behaviorで解説しますが、 基本的な使い方をここで紹介します。

BehaviorExample.java
Label label = new Label("msg", "Hello");

// CSS class を追加する Behavior
label.add(AttributeModifier.append(
    "class", "important"));

// ツールチップを追加
label.add(new AttributeAppender(
    "title",
    Model.of("これはメッセージです")));

// カスタム Behavior の例
label.add(new Behavior() {
    @Override
    public void onComponentTag(
            Component component,
            ComponentTag tag) {
        super.onComponentTag(component, tag);
        tag.put("data-custom", "value");
    }
});

add(label);
レンダリング結果
<!-- 元のHTML -->
<span wicket:id="msg">
  placeholder
</span>

<!-- レンダリング結果 -->
<span class="important"
      title="これはメッセージです"
      data-custom="value">
  Hello
</span>

よく使われる組み込み Behavior の一覧:

Behavior クラス 用途 使用例
AttributeModifier HTML属性を設定・置換する AttributeModifier.replace("class", "active")
AttributeAppender HTML属性に値を追加する AttributeModifier.append("class", "new-class")
AjaxEventBehavior JavaScriptイベントでAJAX呼び出し クリック、マウスオーバーなどのイベントハンドリング
AjaxSelfUpdatingTimerBehavior 一定間隔で自動更新 リアルタイムダッシュボード、ステータス表示

wicket:message と国際化

Wicket は国際化(i18n)を標準でサポートしています。 Java の ResourceBundle と同様に .properties ファイルを使用しますが、 Wicket 独自の仕組みとして、プロパティファイルをページクラスやコンポーネントクラスに紐づけて管理できます。

プロパティファイルの配置

プロパティファイルは、対応する Java クラスと同じパッケージに同名で配置します。 ロケール別のファイルを用意すると、ユーザーのブラウザ言語設定に応じて自動的に切り替わります。

HomePage.properties(デフォルト/日本語)
greeting=\u3053\u3093\u306B\u3061\u306F\u3001\u4E16\u754C\uFF01
app.title=\u79C1\u306E\u30A2\u30D7\u30EA
user.welcome=\u3088\u3046\u3053\u305D\u3001${name}\u3055\u3093
items.count=${count}\u4EF6\u306E\u30A2\u30A4\u30C6\u30E0
HomePage_en.properties(英語)
greeting=Hello, World!
app.title=My Application
user.welcome=Welcome, ${name}
items.count=${count} items

上記の .properties ファイルでは日本語をUnicodeエスケープで記述していますが、 Wicket 10 はUTF-8 エンコーディングのプロパティファイルもサポートしています。 UTF-8で記述する場合は、Application#init()getResourceSettings().setPropertiesFactory(...) の設定が必要になる場合があります。

ファイル配置のイメージ:

src/main/java/
  com/example/pages/
    HomePage.java
    HomePage.html
    HomePage.properties          ← デフォルト(日本語)
    HomePage_en.properties       ← 英語
    HomePage_zh.properties       ← 中国語

wicket:message タグによるメッセージ表示

HTML テンプレート内で <wicket:message> タグを使うと、 Java コードを書かずにプロパティファイルの値を直接埋め込めます。

HomePage.java
public class HomePage extends WebPage {
    public HomePage() {
        // wicket:message を使う場合、
        // Java側でのLabel追加は不要

        // Java側で getString() を使う場合
        String greeting = getString("greeting");
        add(new Label("greetLabel",
            greeting));

        // パラメータ付きメッセージ
        // "user.welcome=ようこそ、${name}さん"
        HashMap<String, Object> params =
            new HashMap<>();
        params.put("name", "田中");
        String welcome =
            getString("user.welcome",
                null, params);
        add(new Label("welcomeLabel",
            welcome));
    }
}
HomePage.html
<html xmlns:wicket="http://wicket.apache.org">
<body>
  <!-- wicket:message でプロパティ値を直接表示 -->
  <h1>
    <wicket:message key="app.title">
      アプリ名
    </wicket:message>
  </h1>

  <!-- 属性値にも使える -->
  <input type="text"
    wicket:message="placeholder:greeting" />

  <!-- Java側でgetStringした結果を表示 -->
  <p wicket:id="greetLabel">挨拶</p>
  <p wicket:id="welcomeLabel">歓迎</p>
</body>
</html>

レンダリング結果(日本語ロケールの場合):

<h1>私のアプリ</h1>
<input type="text" placeholder="こんにちは、世界!" />
<p>こんにちは、世界!</p>
<p>ようこそ、田中さん</p>

Wicket のプロパティファイルは、以下の順序で検索されます(上から優先):

  1. コンポーネントクラスに紐づくファイル(例: MyPanel.properties
  2. ページクラスに紐づくファイル(例: HomePage.properties
  3. Application クラスに紐づくファイル(例: WicketApplication.properties
  4. 親クラスのファイル(継承階層をたどる)

この仕組みにより、アプリケーション全体の共通メッセージは Application レベルに、 ページ固有のメッセージはページクラスレベルに配置するという階層的な管理が可能です。

<wicket:message> タグ内のテキスト(上の例では「アプリ名」)は、 プロパティキーが見つからなかった場合のフォールバックではありません。 キーが見つからない場合は例外がスローされます。 このテキストは、デザイナーがブラウザでHTMLを直接開いたときのプレビュー用です。

実務でハマりやすい注意点

  • 問題
    Java 側でIDを付けたつもりでも、実際のHTMLにそのIDが出ず、JavaScriptやAjax更新で参照できない。デバッグ時に「要素が見つからない」状態になる。
    原因
    setMarkupId(...) だけ設定し、IDを出力する前提の setOutputMarkupId(true) を設定していない。値指定と出力有効化は別設定。
    対策
    AjaxやJSで触るコンポーネントには、まず setOutputMarkupId(true) を付ける。そのうえで必要なら固定IDを指定する。
  • 問題
    新規タブで開く機能が、ブラウザやクリックタイミングによって失敗する。利用者から「開いたり開かなかったりする」と報告されやすい。
    原因
    Ajax 経由でタブ起動すると、ブラウザのポップアップ制御に引っかかることがある。ユーザー操作として認識されないケースがあるため。
    対策
    新規タブ遷移は bookmarkable URL を通常リンクで開く設計にする。ブラウザ標準の挙動に寄せると安定する。