はじめに

HTML5で<input type="file" />multiple属性が追加され、ブラウザのファイル選択画面で複数のファイルが一度に選択でき、サーバへの送信も一度にできるようになった。
SAStrutsはHTML5時代に作られたものではないので、ActionFormでのファイル受取はファイルが一つしか来ないものとして実装されている。フレームワーク部分を拡張してファイルを複数受け取れるようにする。

jspとActionForm

sample.jsp

<input type="file" name="fileDatas" multiple="multiple" />

SampleForm.java

public class SampleForm implements Serializable {

    private FormFile[] fileDatas; // getter, setter 略
}

ファイルを受け取る箇所は、FormFileの配列にする。

S2MultipartRequestHandler

リクエスト内容をActionFormに詰めるための前処理としてリクエスト内容の分解と解釈を、SAStrutsで用意されているS2MultipartRequestHandlerクラスが行っている。addTextParameteraddFileParameterというメソッドで、それぞれリクエストの文字列とファイルをActionFormに詰めるための前処理を行っている。文字列のほうは配列に対応できるように全て配列として扱っているのだが、ファイルの方は単体としてしか扱っていないため、addTextParameterを参考にしてaddFileParameterをOverrideする。

import org.apache.commons.fileupload.FileItem;
import org.apache.struts.upload.FormFile;
import org.seasar.struts.upload.S2MultipartRequestHandler;

public class MyS2MultipartRequestHandler extends S2MultipartRequestHandler {

    @SuppressWarnings("unchecked")
    @Override
    protected void addFileParameter(FileItem item) {
        String name = item.getFieldName();
        FormFile value = new S2FormFile(item);

        FormFile[] oldArray = (FormFile[]) elementsFile.get(name);
        FormFile[] newArray;
        if (oldArray != null) {
            newArray = new FormFile[oldArray.length + 1];
            System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
            newArray[oldArray.length] = value;
        } else {
            newArray = new FormFile[] { value };
        }
        elementsFile.put(name, newArray);
        elementsAll.put(name, newArray);
    }

}

S2RequestProcessor

S2RequestProcessorは、数あるActionのうち何を使うか等を解決し、processActionPerformメソッドで実際にActionのメソッドを呼び出す。SAStrutsでの基幹部分となるクラスだ。
S2MultipartRequestHandlerS2RequestProcessorprocessPopulateメソッドに呼び出されている。このメソッドの終盤でsetPropertyメソッドが呼ばれ、ActionFormの各変数にS2MultipartRequestHandlerで前処理された値をセットしている。

S2MultipartRequestHandler#addFileParameterをOverrideしたため、S2RequestProcessor#setProperty内においても、FormFileを単体として扱っていたものが配列として扱わなければいけなくなった。setPropertyで実際に値をセットしている箇所は次の三つのメソッドに切り出されている。

  • setSimpleProperty
  • setMapProperty ※setSimplePropertyから呼ばれる
  • setIndexedProperty

これらのメソッドでString配列がどのように処理されているのかを確認し、String[]の処理と同様にFormFile[]の処理を実装するようOverrideする。
継承したクラスとOverrideしたメソッドは以下の通り。既存の処理にelse if (value instanceof FormFile[])を追加している。
※setIndexedPropertyについては、String[]の処理は行われていないため、Overrideしていない。

public class MyS2RequestProcessor extends S2RequestProcessor {

    /**
     * 単純なプロパティの値を設定します。
     *
     * @param bean
     *            JavaBeans
     * @param name
     *            パラメータ名
     * @param value
     *            パラメータの値
     * @throws ServletException
     *             何か例外が発生した場合。
     */
    @SuppressWarnings("unchecked")
    @Override
    protected void setSimpleProperty(Object bean, String name, Object value) {
        if (bean instanceof Map) {
            setMapProperty((Map) bean, name, value);
            return;
        }
        BeanDesc beanDesc = BeanDescFactory.getBeanDesc(bean.getClass());
        if (!beanDesc.hasPropertyDesc(name)) {
            return;
        }
        PropertyDesc pd = beanDesc.getPropertyDesc(name);
        if (!pd.isWritable()) {
            return;
        }
        if (pd.getPropertyType().isArray()) {
            pd.setValue(bean, value);
        } else if (List.class.isAssignableFrom(pd.getPropertyType())) {
            List<String> list = ModifierUtil.isAbstract(pd.getPropertyType()) ? new ArrayList<String>()
                    : (List<String>) ClassUtil
                            .newInstance(pd.getPropertyType());
            list.addAll(Arrays.asList((String[]) value));
            pd.setValue(bean, list);
        } else if (value == null) {
            pd.setValue(bean, null);
        } else if (value instanceof String[]) {
            String[] values = (String[]) value;
            pd.setValue(bean, values.length > 0 ? values[0] : null);
        } else if (value instanceof FormFile[]) {
            FormFile[] values = (FormFile[]) value;
            pd.setValue(bean, values.length > 0 ? values[0] : null);
        } else {
            pd.setValue(bean, value);
        }
    }

    /**
     * Mapの値を設定します。
     *
     * @param map
     *            マップ
     * @param name
     *            キー名
     * @param value
     *            値
     */
    @SuppressWarnings("unchecked")
    @Override
    protected void setMapProperty(Map map, String name, Object value) {
        if (value instanceof String[]) {
            String[] values = (String[]) value;
            map.put(name, values.length > 0 ? values[0] : null);
        } else if (value instanceof FormFile[]) {
            FormFile[] values = (FormFile[]) value;
            map.put(name, values.length > 0 ? values[0] : null);
        } else {
            map.put(name, value);
        }
    }
}

※ActionForm内でFormFileを配列で定義しているときは、S2MultipartRequestHandlerの拡張をするだけでも動くのだが、FormFileを単体で定義すると動かなくなってしまう。上に書いた拡張を行えば単体でも動く。

struts-config.xml

S2MultipartRequestHandlerS2RequestProcessorstruts-config.xmlcontrollerタグで設定することになっているが、独自クラスを継承して作ったため、明示的に設定が必要になる。

略

<controller
    maxFileSize="250M"
    bufferSize="1024"
    processorClass="jp.co.sample.framework.processor.MyS2RequestProcessor"
    multipartClass="jp.co.sample.framework.handler.MyS2MultipartRequestHandler"
/>

略

processorClassmultipartClassにそれぞれ継承実装したクラスを設定しているのは見ての通りとして、maxFileSizeについては注意が必要となる。
maxFileSizeはアップロードされるファイルのサイズを制限する数値だが、ファイルを複数アップロードした時はファイル一つ一つに制限値が適用されるのではなく、複数まとめたサイズに対して適用される。
ファイルを一つだけアップロードした時には十分だった値も、ファイルを複数アップロードするとなると途端に制限が厳しすぎるということになりかねないので、適切な値に設定し直す必要がある。