アップロードされたファイルの拡張子とサイズをチェックする方法

- 環境
    - CentOS Linux release 7.8.2003 (Core)
    - Eclipse IDE for Enterprise Java Developers.Version: 2020-03 (4.15.0)
    - openjdk version "11.0.7" 2020-04-14 LTS
    - JSF 2.3.9

# やりたいこと
1. アップロードされたファイルの拡張子が指定のもの以外の場合はエラーにしたい
2. アップロードされたファイルのサイズが指定より大きかった場合はエラーにしたい
3. エラーメッセージは親画面で指定したい

Fileインターフェースから名前やサイズを取り出してチェック

> File オブジェクトは特別な種類の Blob オブジェクトであり、 Blob が利用できる場面ではどこでも利用できます。
> [File - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/File)


# ファイルの拡張子が指定のもの以外の場合はエラーにしたい

```javascript:upload.js
/**
 * 拡張子が正しいか判定する.
 * @param  {string} ファイル名.
 * @return {Boolean} true:正しい.
 */
function isCorrectExtension(name) {
    // スペース以外の文字で始まって「.jpg」「.png」「.gif」「.psf」で終わる文字(大文字・小文字を区別しない[i])
    var format = new RegExp('([^\s]+(\\.(jpg|png|gif|pdf))$)', 'i');
    return format.test(name);
}
```

|特殊文字|意味|
|:-:|:-:|
|^|入力の先頭にマッチ|
|$|入力の末尾にマッチ|
|\s|スペース、タブ、改ページ、改行を含むホワイトスペース文字にマッチ|

- 参考
    - [イメージファイルの拡張子を正規表現で検証する方法](https://www.codeflow.site/ja/article/regular-expressions__how-to-validate-image-file-extension-with-regular-expression)
    - [正規表現 - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions)
    - [正規表現（RegExp） - とほほのWWW入門](http://www.tohoho-web.com/js/regexp.htm#RegFlags)
    - [Javascript での 正規表現 の エスケープ について - Qiita](https://qiita.com/ue5963/items/bd8e32ac9e6b12aa7fab)


# ファイルのサイズが指定より大きかった場合はエラーにしたい
```javascript:upload.js
/**
 * ファイルサイズが正しいかを判定する.
 * @param  {number} ファイルサイズ(バイト単位).
 * @return {Boolean} true:正しい.
 */
function isCorrectSize(size) {
    /** @type {number} 許容する最大サイズ(1MB). */
    var maxSize = 1024 * 1024;
    return size <= maxSize;
}
```

- 参考 : [バイト換算 - 高精度計算サイト](https://keisan.casio.jp/exec/user/1355670543)

# エラーメッセージは親画面で指定したい
状況に合わせて使えるように思い付いた方法３つ
## 方法1. JavaScriptのwindow.openerで親画面から取得する
1. エラーメッセージを親画面の隠し項目で設定しておく
2. 子画面のJavaScript処理で`window.opener`を使って取得する

- 参考
    - [子窓から親窓を操作 | 私的雑録](http://php.o0o0.jp/article/jquery-window_opener)
    - [[JavaScript] サブウィンドウから親ウィンドウを参照する（window.opener） | コピペで使える JavaScript逆引きリファレンス](https://javascript.programmer-reference.com/js-window-opener/)

```xml:親画面
...省略...
<h:inputHidden id="extErrMessage" value="拡張子が対象外だよ。" />
<h:inputHidden id="sizeErrMessage" value="ファイルサイズが大きすぎるよ。" />
...省略...
```

```javascript:upload.js
...省略...
        if (!isCorrectExtension(file.name)) {
            errMessage += window.opener.$('#extErrMessage').text();
        }
        if (!isCorrectSize(file.size)) {
            if (errMessage != '') {
                errMessage += '<br />';
            }
            errMessage += window.opener.$('#sizeErrMessage').text();
        }
...省略...
```

## 方法2. 子画面を表示するときにパラメータでメッセージを渡す
1. 親画面で子画面を表示するJavaSctiptを生成するときにエラーメッセージをGETのパラメータで設定する
1. 子画面を開いたらパラメータを`f:viewParam`で受け取ってバッキングビーンに設定する
2. バッキングビーンのエラーメッセージをJSON形式で置いておく
3. JavaScriptで`parseJSON`を使ってエラーメッセージを取得する

```xml:親画面
...省略...
<input type="button" value="アップロード" onclick="#{uploadBean.onClick}" />
...省略...
```

```java:UploadBean.java
    /**
     * onClick属性用に出力するJavaScriptコードを取得する.
     * @return JavaScriptコード.
     */
    public String getOnClick() {
        StringBuilder builder = new StringBuilder();
        builder.append("window.open('upload.jsf");
        builder.append("?key=");
        builder.append(urlEncode("formId:file"));
        builder.append("&extErrMessage=");
        builder.append(urlEncode("拡張子が対象外だよ。"));
        builder.append("&sizeErrMessage=");
        builder.append(urlEncode("ファイルサイズが大きすぎるよ。"));
        builder.append("', '', 'width=500,height=100'); return false;");
        return builder.toString();
    }

    /**
     * orgをURLエンコードして返す.
     * @param org
     * @return
     */
    private String urlEncode(String org) {
        try {
            return URLEncoder.encode(org, "utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
```

```xml:upload.xml(子画面)
...省略...
  <f:metadata>
    <ui:remove>GETのパラメータを受け取る</ui:remove>
    <f:viewParam name="key" value="#{uploadBean.key}"/>
    <f:viewParam name="extErrMessage" value="#{uploadBean.extErrMessage}" />
    <f:viewParam name="sizeErrMessage" value="#{uploadBean.sizeErrMessage}" />
  </f:metadata>
  <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  <h:outputScript library="js" name="upload.js"/>
  <ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove>
  <script id="errMessage" type="application/json">
    {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"}
  </script>
...省略...
```

```javascript:upload.js
...省略...
        /** @type {array} headタグ内に置いておいたエラーメッセージ. */
        var message = $.parseJSON($('#errMessage').html());
        /** @type {object} 選択されたファイル. */
        var file = inputFile.files[0];
        if (!isCorrectExtension(file.name)) {
            errMessage += message.ext;
        }
        if (!isCorrectSize(file.size)) {
            if (errMessage != '') {
                errMessage += '<br />';
            }
            errMessage += message.size;
        }
...省略...
```

## 方法3. 親子画面で同じバッキングビーンを使う
1. 親子画面で共通のバッキングビーンにエラーメッセージ取得処理を実装する
2. あとは「方法2. 子画面を表示するときにパラメータでメッセージを渡す」の「バッキングビーンのエラーメッセージをJSON形式で置いておく」以降と同じ


```java:UploadBean.java
...省略...
    /**
     * 拡張しでエラーになった時のエラーメッセージを取得する.
     * @return エラーメッセージ.
     */
    public String getExtErrMessage() {
        return "拡張子が対象外だよ。";
    }

    /**
     * サイズでエラーになった時のエラーメッセージを取得する.
     * @return エラーメッセージ.
     */
    public String getSizeErrMessage() {
        return "ファイルサイズが大きすぎるよ。";
    }
...省略...
```

```xml:upload.xml(子画面)
  <ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove>
  <script id="errMessage" type="application/json">
    {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"}
  </script>
```



# 実装全体

```xml:親画面
<?xml version='1.0' encoding='UTF-8' ?>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
  xmlns:h="http://xmlns.jcp.org/jsf/html"
  xmlns:f="http://xmlns.jcp.org/jsf/core">
<ui:composition template="template.xhtml">
<ui:define name="js">
  <h:outputScript library="js" name="upload.js"/>
</ui:define>
<ui:define name="content">
  <h3>ファイルの入力チェックをしてみる</h3>
  <h:form id="formId">
    <div id="uploadArea">
      <ui:fragment rendered="#{!uploadBean.upload}">
        <h:button value="アップロード" onclick="showPopup();"/>
        <h:inputText id="file" style="display:none;">
          <f:ajax event="change" execute="@form" render="@form" listener="#{uploadBean.uploadFile}" />
        </h:inputText>
      </ui:fragment>
      <ui:fragment rendered="#{uploadBean.upload}">
        <h:outputText value="#{uploadBean.file.name}" />
        <h:commandButton value="削除">
          <f:ajax execute="@form" render="@form" listener="#{uploadBean.deleteFile}" />
        </h:commandButton>
      </ui:fragment>
      <div><h:message for="uploadArea" errorClass="error" warnClass="warn" infoClass="info" /></div>
    </div>
  </h:form>
</ui:define>
</ui:composition>
</html>
```

```xml:upload.xhtml(子画面)
<?xml version='1.0' encoding='UTF-8' ?>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
  xmlns:h="http://xmlns.jcp.org/jsf/html"
  xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
  <title>アップロードするファイル</title>
  <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  <h:outputScript library="js" name="upload.js"/>
  <ui:remove>エラーメッセージをJSON形式で置いておく</ui:remove>
  <script id="errMessage" type="application/json">
    {"ext" : "#{uploadBean.extErrMessage}", "size" : "#{uploadBean.sizeErrMessage}"}
  </script>
</h:head>
<body>
  <div>
    <h:inputFile id="inputFile" onchange="checkFile(this)" value="uploadBean.file" />
  </div>
  <div>
    <h:button value="OK" onclick="submit('#{uploadBean.key}');" />
    <h:button value="閉じる" onclick="window.close();" />
  </div>
</body>
</html>
```

```javascript:upload.js
/** ポップアップ画面を表示する. */
function showPopup() {
    window.open('upload.jsf', '', 'width=500,height=100');
}
/**
 * アップロードされたファイルをチェックする.
 * @param {Object} Fileオブジェクト.
 */
function checkFile(inputFile) {
    // エラーメッセージを削除する.
    $('.errMessage').remove();
    /** @type {String} 表示するエラーメッセージ. */
    var errMessage = '';
    if (inputFile.files && inputFile.files[0]) {
        /** @type {array} headタグ内に置いておいたエラーメッセージ. */
        var message = $.parseJSON($('#errMessage').html());
        /** @type {object} 選択されたファイル. */
        var file = inputFile.files[0];
        if (!isCorrectExtension(file.name)) {
            errMessage += message.ext;
        }
        if (!isCorrectSize(file.size)) {
            if (errMessage != '') {
                errMessage += '<br />';
            }
            errMessage += message.size;
        }
    }
    if (errMessage != '') {
        // エラーメッセージを追加する.
        $('#inputFile').after('<br /><span class="errMessage" style="color: red;">' + errMessage + '</span>');
        // ファイルを削除する.
        inputFile.value = null;
    }
}

/**
 * 拡張子が正しいか判定する.
 * @param  {string} ファイル名.
 * @return {Boolean} true:正しい.
 */
function isCorrectExtension(name) {
    var format = new RegExp('([^\s]+(\\.(jpg|png|gif|pdf))$)', 'i');
    return format.test(name);
}

/**
 * ファイルサイズが正しいかを判定する.
 * @param  {number} ファイルサイズ(バイト単位).
 * @return {Boolean} true:正しい.
 */
function isCorrectSize(size) {
    /** @type {number} 許容する最大サイズ(1MB). */
    var maxSize = 1024 * 1024;
    return size <= maxSize;
}

/**
 * 親画面の要素を更新して画面を閉じる.
 * @param  {string} key 更新する親画面要素のid.
 */
function submit(key) {
    window.opener.$('#'+key.replace(/:/g,"\\:")).change();
    window.close();
}
```

```java:UploadBean.java
package brans;

import java.io.IOException;
import java.io.Serializable;

import javax.faces.view.ViewScoped;
import javax.inject.Named;
import javax.servlet.http.Part;

import lombok.Data;

@Named
@ViewScoped
@Data
public class UploadBean implements Serializable {
    /** serialVersionUID. */
    private static final long serialVersionUID = -355651229394801584L;
    /** ファイルデータ. */
    private Part file;

    /**
     * ファイルがアップロードされているかを判定する.
     * @return true:アップロードされている.
     */
    public boolean isUpload() {
        return this.file != null;
    }

    /**
     * 拡張しでエラーになった時のエラーメッセージを取得する.
     * @return エラーメッセージ.
     */
    public String getExtErrMessage() {
        return "拡張子が対象外だよ。";
    }

    /**
     * サイズでエラーになった時のエラーメッセージを取得する.
     * @return エラーメッセージ.
     */
    public String getSizeErrMessage() {
        return "ファイルサイズが大きすぎるよ。";
    }

    public String getKey() {
        return "formId:file";
    }

    public void uploadFile() throws IOException {
        if (!isUpload()) {
            return;
        }
        if (!isCorrectExtension(this.file.getName())) {
            deleteFile();
        }
        if (!isCorrectSize(this.file.getSize())) {
            deleteFile();
        }
    }

    /**
     * アップロードしたファイルを削除する.
     * @throws IOException エラーが起きた.
     */
    public void deleteFile() throws IOException {
        this.file.delete();
    }

    /**
     * 拡張子が正しいか判定する.
     * @param name ファイル名.
     * @return true:正しい.
     */
    private boolean isCorrectExtension(String name) {
        if (name != null) {
            return name.matches("([^\\s]+(\\.(?i)(jpg|png|gif|pdf))$)");
        }
        return true;
    }

    /**
     * ファイルサイズが正しいかを判定する.
     * @param size ファイルサイズ(バイト).
     * @return true:正しい.
     */
    private boolean isCorrectSize(long size) {
        long maxSize = 1024 * 1024;
        return size <= maxSize;
    }
}
```