[ 2747 ] [Struts2]AjaxUpload+プログレスバー

※こちらの記事はStruts2.3系で修正されました、最新記事[2785]を参照してください※

Ajaxを利用したアップロードの仕組みができましたので公開します。

あまりStruts2の機能は使っていませんが…

この記事はStruts2.3以降のバージョンでは正しく動作しません。
Struts2でAjaxを利用した巨大ファイルアップロードとプログレスバーの実装をご覧ください!


Struts2の標準Uploadの機能を少し拡張したものと、jQueryのライブラリを使っています。
プログレスバーを使ったアップロードの進み具合も表示できるようにしていますので、1秒程度の感覚で更新をさせる機能もjQueryで行っています。

利用したライブラリはこちらになります。
※今回公開しているEclipseのプロジェクトには全て含まれています。

http://valums.com/ajax-upload/

enfranchisedmind:jquery-periodicalupdater-ajax-polling

プロジェクトはこちらです。
mybatisSample.zip
※プロジェクトファイルは、m2eclipseプラグインを利用したmaven2プロジェクトになっています。m2eclipseの導入については[Struts2]m2eclipseの導入を参照してください。

Struts2でのアップロードの基本は…

アップロードの基本はこちらになります。
[ 2563 ] [Struts2]アップロード(再掲載)

別途、Commons-Uploadが必須になります。
※今回のサンプルではmaven2で自動的に組み込まれます。



他に決められているルールとして、、

送信フォームのFileパラメータ名が、
アップロード用Actionのフィールド名になる。



例:
Fileタグのパラメータ名:myDoc
Actionクラスのフィールド:
private File myDoc;
private String myDocContentType;
private String myDocFileName;


Commons-FlieUploadにはアップロード状況を確認できるリスナーが搭載されている

一言で言ってしまうとそうなんですけれど、実装する際には多少気にしなければならないことがあります。

必要なこと:
org.apache.commons.fileupload.ProgressListenerを実装



実際のクラスは以下のようになります。別途、set/getを用意しましょう。

public class FileUploadProgressListener implements ProgressListener {

  private long readSize = -1L;
  private long bytesRead = 0L;
  private long bufferSize = 8192L;
  private boolean complete;
  private long contentLength;
  private long transferedSize = 0L;
  private long fileSize = 0L;

  /**
   * implements method.
   */
  public void update(long pBytesRead, long pContentLength, int pItems) {
    // 過去読み込み済みサイズが、読み込みサイズと一緒の場合は何もしない。バッファサイズ単位で比較する。
    if ( readSize / bufferSize == pBytesRead / bufferSize ) {
      return;
    }

    // ファイルアップロード済みバイト数
    bytesRead = pBytesRead;

    // 実際にアップロードされるファイルのサイズ(バイト)
    setContentLength(pContentLength);

    // アップロードによるファイル転送が完了したかを判定する
    complete = ( bytesRead == pContentLength) && (bytesRead >= 0);

    /*
    log.debug("bytesRead:"+ pBytesRead);
    log.debug("contentLength:"+ pContentLength);
    log.debug("items:"+ pItems);
    log.debug("isComplete:"+ complete);
    */

    transferedSize = pBytesRead;
    if ( fileSize != pContentLength ) fileSize = pContentLength;
  }

  /**
   * リスナーがリクエスト属性へバインドするサイズ間隔を設定する。
   * 未設定時は8kBごと。引数にはバイト単位で設定する。
   * @param bufferSize セットする bufferSize
   */
  public void setBufferSize(long bufferSize) {
    this.bufferSize = bufferSize;
  }
}



大切なのは、一定量ごとにチェックしないと、物凄く少ないバイト数(8kB)ごとにチェックしてしまいます。それですとかなり無駄になりますので、バッファサイズごとに比較するようにしています。

Struts2のUpload用のリクエストマッパーを変更する

今回一番大切なことがこれです。

Struts2は標準でアップロード用の便利な機能が搭載されていますが、今回これを拡張します。
これを拡張する理由は、先ほど設定したアップロード用のリスナーから取得しておくことで、アップロードを非同期リクエストでポーリングしてその進捗状況を実際にアップロードで送信されたバイト数を容易に取得できるからです。

具体的には、デフォルトで利用されているアップロードのマッパークラスを継承し、sturts.xmlの設定で変更します。

以下に示すコードは、デフォルトのMultiPartRequestを継承したクラスです。

public class UploadListenerAddedJakartaMultiPartRequest
  implements MultiPartRequest {
  FileUploadProgressListener progressListener;
  …(省略)…
  private List<FileItem> parseRequest(
    HttpServletRequest servletRequest, String saveDir) 
    throws FileUploadException {
        DiskFileItemFactory fac = createDiskFileItemFactory(saveDir);
        ServletFileUpload upload = new ServletFileUpload(fac);
        upload.setSizeMax(maxSize);

        // A-pZ added : binding upload listener.
        progressListener = new FileUploadProgressListener();
        progressListener.setBufferSize(1024*1024);
        upload.setProgressListener(progressListener);

        servletRequest.getSession().setAttribute("__uploadListener", progressListener);
        // A-pZ added : binding upload listener.

        return upload.parseRequest(createRequestContext(servletRequest));
    }
}



これをstruts.xmlにて利用する設定を行います。

<struts>
  <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest"
        name="xjakarta"
        class="seren.lumi2.interceptor.UploadListenerAddedJakartaMultiPartRequest"
        scope="default" />
    <constant name="struts.multipart.handler" value="xjakarta" />
</struts>



エイリアス:xjakartaで先ほど作成したマッパークラスを定義し、struts.multipart.handlerをxjakartaにする、と宣言していますね(ω・

Actionクラスのインターセプタ

jQueryを使って進捗状況をAjaxで受け取るようにしましたので、今回はActionクラスからはJSONプラグインを使ってJSONをレスポンスをします。

そのインターセプタはAjax用に設定します。

@Namespace("/upload")
@ParentPackage("json-default")
@InterceptorRefs({
  @InterceptorRef("fileUpload"),
  @InterceptorRef("basicStack"),
  @InterceptorRef("json"),
})



特に変わったことはしていませんが、JSONプラグインを使いますので、json-defaultを継承して@InterceptorRef("json")を使えるようにします。

JSP+jQueryの数々

最後にJSPです。

jQuery+Periodical Updater+jQuery Progress Barを利用したものになっています。
これらは全て同梱されています。

<?xml version="1.0" encoding="utf-8" ?>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" isELIgnored="false"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<%@ taglib prefix="sj" uri="/struts-jquery-tags"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Struts2 ajax-upload</title>
<sj:head jqueryui="true" jquerytheme="custom-theme"
    customBasepath="../template/themes" ajaxcache="false"
    compressed="false" />
<script type="text/javascript" src="../js/jquery.periodicalupdater.js"></script>
<script type="text/javascript" src="../js/jquery.progressbar.js"></script>
<script type="text/javascript" src="../js/ajaxupload.3.5.js"></script>
<script type="text/javascript">
    $.ajaxSetup({
      cache: false
    });

    $(document).ready(function(){
      new AjaxUpload('upload_button', {
          action: 'upload' ,
          name:'myDoc',
          data: {
              example_key1 : 'example_value',
              example_key2 : 'example_value2',
              blocker : 'blockerValue'
            },
          autoSubmit: true,
          responseType: 'json',
          onChange: function(file, extension){},
          onSubmit: function(file, extension) {
            //this.disable();

            $("#upload_button").addClass("ui-state-disabled");
            $("#upload_button").attr('disabled','disabled');

            $("div#result").text('アップロード中');
            $("#pb1").progressBar(0 , { barImage: '../images/progressbg_orange.gif',boxImage: '../images/progressbar.gif'});

            var actionUrl = "check";

            $.PeriodicalUpdater(actionUrl, {
                method:'get',
                minTimeout: 500,
                maxTimeout: 8000,
                multiplier: 2,
                type      : 'json' },
                function(progress) {
                if ( progress == null ) return false;

                  if ( progress.contentLength != 0 ) {
                      // アップロード中バイト数 / ファイルサイズ
                    $("div#result").text(progress.readLength + "/" + progress.contentLength);
                  }

                  var percentage = Math.floor(100 * parseInt(progress.readLength) / parseInt(progress.contentLength));
                  $("#pb1").progressBar(percentage , { barImage: '../images/progressbg_orange.gif',boxImage: '../images/progressbar.gif'});

                  if ( progress.isComplete ) {
                    $("#pb1").progressBar(100 , { barImage: '../images/progressbg_black.gif',boxImage: '../images/progressbar.gif'});
              return true;
                  }

                      return false;
                }
            );
            },
          onComplete: function(file, response) {
              $("div#result").text(response);
              $("#pb1").progressBar(100 , { barImage: '../images/progressbg_green.gif',boxImage: '../images/progressbar.gif'});
              $("#upload_button").removeClass("ui-state-disabled");
              $("#upload_button").attr('disabled','');
            }
        });
    });

</script>
<style type="text/css">
</style>
</head>
<body>
<s:i18n name="commons">
    <button id="upload_button"
        class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only">
    <span id="upload_button_text" class="ui-button-text">Upload</span></button>
    <div id="result"></div>
    <span class="progressBar" id="pb1"></span>
</s:i18n>
</body>
</html>





loading...