2010年09月07日

vaadinでファイルダウンロード

vaadinでファイルのダウンロードは簡単すぎるせいなのか、Samplerにも用意されていません。
私としてはメモをどこかにまとめてくれてあったらありがたいと思ったので、ここに記載しておきます。

基本的には、Window#open() メソッドを利用します。
このメソッドは、対象のリソースをブラウザに表示します。
ブラウザで表示ではなく、プログラムで開くか保存するかの選択ダイアログでの出力が目的なので、FileResource に Content-Disposition ヘッダを指定するようにします。
具体的には、FileResource を継承して getStream() をオーバーライドします。
ほとんど FileResource と同じですが、setParameter() で Content-Disposition ヘッダを指定する処理を追加しています。
public class FileDownloadResource extends FileResource {
  public FileDownloadResource(File sourceFile, Application application) {
    super(sourceFile, application);
  }

  @Override
  public DownloadStream getStream() {
    try {
      final DownloadStream ds = new DownloadStream(new FileInputStream(
                                     getSourceFile()), getMIMEType(), getFilename());
      ds.setParameter("Content-Disposition", "attachment; filename*=" + getFilename());
      ds.setCacheTime(getCacheTime());
      return ds;
    } catch (final FileNotFoundException e) {
      return null;
    }
  }
}
呼び出しは下記のようになります。
  ServletContext servletContext
    = ((WebApplicationContext) getContext()).getHttpSession().getServletContext();
  File file = new File(servletContext.getRealPath("お試しファイル.txt"));
  getMainWindow().open(new FileDownloadResource(file, getApplication()));
これで保存するか開くかの確認ダイアログが開きます。
このままめでたしめでたしのはずでしたが、やはり何かが起こるものです。

ダイアログに表示されているファイル名が文字化けしています。
Internet Explorere 8.0 や Google Chrome 6.0、Safari 5.0 では文字化けが起こらず、FireFox 3.6 のみが文字化けが起こります。
普段私が使うブラウザが文字化けでは困るので何とか対策を考えてみます。
Content-Disposition ヘッダのファイル名に対する処理が、各ブラウザごとに異なるのが原因のようです。
ここから先はvaadinの話ではないですが、乗りかかった船なので解決するまで進んでみます。
ネットで解決策を検索してみると、「ブラウザごとに処理を変えて・・・」とありましたが、 そんなことはしたくないですね。
※ 参考にしたページの動作確認と、私の確認方法が異なるようですが、この件は深くは追いません。
上記すべてのブラウザで日本語ファイル名が正しく表示できた方法を記載します。
URLエンコードしたファイル名を用い、ファイル名の前に「filename*=utf8'URLエンコードされたファイル名」とします。
上記 getStream() の該当箇所を下記ソースに置き換えてください。
  String encodedFileName = URLEncoder.encode(getFilename(), "UTF-8");
  ds.setParameter("Content-Disposition", "attachment; filename*=utf8'" + encodedFileName);
まだ文字化けの件は、古いブラウザなどすべてを見切れていませんが、問題が出てくるまではこのソース利用してみます。
タグ:vaadin
posted by しん at 00:00| Comment(0) | vaadin | このブログの読者になる | 更新情報をチェックする

2010年09月06日

vaadinでちょっとはまりました コンポーネントの制御

またしても罠にはまってしまいました。
前回の画面制御に関連して、コンポーネントの持ち方について理解が足りなかったせいで、いくら登録してもコンポーネントが画面に表示されない状態となりました。
どんなことをしていたかと言うと、
 @一種のウィザードのような2ページで遷移する画面を作成
 A2ページ共通で使うコンポーネントとしてキャンセルボタンを用意
 B各ページにキャンセルボタンを登録
上記のようなことをしていました。
ページの制御は前回の方法をとっています。
removeComponent / addComponent を用いて、直接制御を行っています。
2ページに共通するコンポーネントなので、下記のようなコーディングをしてみました。
結果は最初に記述したように、キャンセルボタンが1ページ目に表示されません。
public class SubWindow extends Window {
  private Layout contextLayout;
  private Layout firstPage;
  private Layout secondPage;
  private Button cancelButton;
  ・・・
  public SubWindow() {
    contextLayout = (VerticalLayout) getContent();
    firstPage = createFirstPage();
    secondPage = createSecondPage();
    cancelButton = new Button("キャンセル", new Button.ClickListener() {
      public void buttonClick(ClickEvent event) {
        ((Window) getParent()).removeWindow(getWindow());
      }
    });
    mainWindow.addComponent(firstPage);

    setMainWindow(mainWindow);
  }
  private Layout createFirstPage() {
    VerticalLayout layout = new VerticalLayout();
    Button button = new new Button("次へ", new Button.ClickListener() {
      public void buttonClick(ClickEvent event) {
        ・・・
        switchPage("secondPage");
      }
    });
    layout.addComponent(button);
    layout.addComponent(cancelButton);
    return layout;
  }
  private Layout createSecondPage() {
    VerticalLayout layout = new VerticalLayout();
    Button button = new Button("実行", new Button.ClickListener() {
      public void buttonClick(ClickEvent event) {
        ・・・
        switchPage("firstPage");
      }
    });
    layout.addComponent(button);
    layout.addComponent(cancelButton);
    return layout;
  }
  private void switchPage(String pageName) {
    if ("firstPage".equals(pageName)){
      contextLayout.addComponent(firstPage);
      contextLayout.removeComponent(secondPage);
    } else {
      contextLayout.removeComponent(firstPage);
      contextLayout.addComponent(secondPage);
    }
  }
}

何が原因なのでしょうか?
一見どこにも問題がなさそうに見えるのですが表示をしてくれません。
2ページ目で表示されるので、キャンセルボタンオブジェクトがなくなってしまったわけではないようです。
最初のページではまだ removeComponent() を実行していないので、怪しいのは addComponent() です。
vaadinのソースを調べてみると、コンポーネントオブジェクトは親(コンテナー)を持っており、コンテナー(親)はコンポーネントのリストを保持して制御をしています。
そこで、addComponent() を確認すると、AbstractComponentContainer#addComponent() では、ご丁寧に追加しようとしているコンポーネントの親(コンテナー)から、自分を削除させています。
コンポーネントは1つのコンポーネントにしか所属できないようです。
createFirstPage() でキャンセルボタンは1ページ目に登録されていますが、次の行でcreateSecondPage() が実行され、このメソッドの中でキャンセルボタンを2ページ目に登録を行っています。
この際にキャンセルボタンの親である1ページ目(Layoutコンテナ)のコンポーネントリストから自分を削除し、2ページ目のコンテナに登録をしています。
そのため、1ページ目に表示してくれなかったわけです。
残念ながら、同じようなコンポーネントでもいちいち作成するようにします。
posted by しん at 00:00| Comment(0) | vaadin | このブログの読者になる | 更新情報をチェックする

2010年09月05日

vaadinでちょっとはまりました フラグメント管理

vaadinで画面を作成しようとしてはまってしまった罠(?)をご紹介します。
フラグメント管理ですっきり画面の制御をしようと、
Javaで軽快に使える「軽量フレームワーク」特集 〜リッチなGUIを構築する「Vaadin」(3)
を参考に試していたところ、サブウィンドウでこのテクニックを使用しようとしてうまく制御ができませんでした。
どんなことをしていたかと言うと、
 @サブウィンドウで一種のウィザードのような2ページで遷移する画面を作成
 Aサブウィンドウの2ページは「UriFragmentUtilityによるフラグメント管理」をもちいる
 Bサブウィンドウは処理を完了すると画面を閉じる
上記のようなことをしていました。
最初にサブウィンドウを開いた際には思い通りの制御ができていたのですが、2度目以降は前回のフラグメントが残っていて、サブウィンドウはいきなり2ページ目が表示されてしまいました。
簡単に要素となる部分のみのコーディングを示すと下記のようになります。
public class SubWindow extends Window {
  private Layout contextLayout;
  private Layout firstPage;
  private Layout secondPage;
  private UriFragmentUtility urifu = new UriFragmentUtility();
  ・・・
  public SubWindow() {
    contextLayout = (VerticalLayout) getContent();
    firstPage = createFirstPage();
    secondPage = createSecondPage();
    addComponent(urifu);
    urifu.addListener(new FragmentChangedListener() {
      public void fragmentChanged(FragmentChangedEvent source) {
        String s = source.getUriFragmentUtility().getFragment();
        if ("firstPage".equals(s)){
          contextLayout.addComponent(firstPage);
          contextLayout.removeComponent(secondPage);
        } else {
          contextLayout.removeComponent(firstPage);
          contextLayout.addComponent(secondPage);
        }
      }
    });
    contextLayout.addComponent(firstPage);
  }
  private Layout createFirstPage() {
    VerticalLayout layout = new VerticalLayout();
    Button button = new Button("次へ", new Button.ClickListener() {
      public void buttonClick(ClickEvent event) {
        ・・・
        urifu.setFragment("secondPage", true);
      }
    });
    layout.addComponent(button);
    return layout;
  }
  private Layout createSecondPage() {
    VerticalLayout layout = new VerticalLayout();
    Button button = new Button("実行", new Button.ClickListener() {
      public void buttonClick(ClickEvent event) {
        ・・・
        ((Window) getParent()).removeWindow(getWindow());
      }
    });
    layout.addComponent(button);
    return layout;
  }
}
2ページめのボタンクリックでフラグメントをセットしても、メイン画面に戻ってきた際にはフラグメントが残ったままになってしまうようです。
また、サブウィンドウのコンストラクタでフラグメントを初期化しても、前回のフラグメントが有効なのか、一瞬にして2ページ目に遷移してしまいます。
深く内部的な部分まで追えませんでしたが、イベントが発生するタイミングや、クライアント側のオブジェクトの持ち方などに関係していそうです。
結局フラグメントのイベントはあきらめ、直接コンポーネントを制御する方法をとりました。
ページ制御を行う関数を用意し、
  private void switchPage(String pageName) {
    if ("firstPage".equals(pageName)){
      contextLayout.addComponent(firstPage);
      contextLayout.removeComponent(secondPage);
    } else {
      contextLayout.removeComponent(firstPage);
      contextLayout.addComponent(secondPage);
    }
  }
ボタンはこの関数を呼ぶように変更します。
    Button button = new Button("次へ", new Button.ClickListener() {
      public void buttonClick(ClickEvent event) {
        ・・・
        switchPage("secondPage");
      }
    });
これで思い通りの動作をしてくれる画面ができました。
よくよくソースを見比べると、フラグメントのチェンジイベントによる制御と、関数呼び出しによる制御で、ほとんど変わっていないことがわかります。
せっかくある技だからと、少し時間をかけすぎたことを反省しています。
posted by しん at 07:48| Comment(0) | vaadin | このブログの読者になる | 更新情報をチェックする
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。