HttpServletでInputStreamを複数回読めるようにしつつmultipart/form-dataのパラメータを取得する

# 対応したい内容

servletにmultipart/form-data形式でPOSTされてきたパラメータを読み取るには`HttpServletRequest.getParts()`を用いる方法[^1]や、下記のように`HttpServletRequest.getParameter()`を用いる方法がある。

```java:TestServlet.java
package test;

import java.io.IOException;
import java.util.Enumeration;

import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@SuppressWarnings("serial")
@WebServlet(name="Test Servlet", urlPatterns={"/*"})
@MultipartConfig()
public class TestServlet extends HttpServlet {

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
		try {
			// multipart/form-data のパラメータを取得する
			for (Enumeration<String> parameters = req.getParameterNames(); parameters.hasMoreElements();) {
				String parameter = parameters.nextElement();
				System.out.println("parameter:" + parameter + ", value:" + req.getParameter(parameter));
			}
			// inputStreamをresponse bodyに出力
			// しかし既にinputStreamはgetParameter()により消費されてしまっているのでbodyには何も書き込まれない。
			req.getInputStream().transferTo(resp.getOutputStream());
		} catch (IOException e) {
			System.out.println(e.getStackTrace());
		}
	}
}
```

しかし`getParts()`や`getParameter()`を使うと`HttpServletRequest.getInputStream()`を消費してしまうので再び読み出すことはできなくなる。`inputStream`は別の用事で使いたい場合もあるのでこれを何とかしたい。

# 対策
こちらの記事[^2]を参考に`getInputStream()`の内容をbyte配列にキャッシュし、そこから毎回`inputStream`を生成するようにする。ただこうすると`getParts()`や`getParameter()`でmultipart/form-dataのパラメータを取得できなくなるので、こちらはApache Commons FileUpload[^3]で読み取るようにする。

# 実装例
基本的には参考リンクのコード[^2][^3]のコピペですが、少なくとも後述の検証環境では`isFinished()`, `isReady()`, `setReadListener`のオーバーライドも必要になったためstuck over flowの記事[^4][^5]を参考にメソッドを追加しています。

```java:BufferedServletRequestWrapper.java
package test;

import java.io.IOException;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class BufferedServletRequestWrapper extends HttpServletRequestWrapper {

	private byte[] buffer;

	public BufferedServletRequestWrapper(HttpServletRequest request) throws IOException {
		super( request );
		buffer = request.getInputStream().readAllBytes();
	}

	@Override
	public ServletInputStream getInputStream() {
		return new BufferedServletInputStream(buffer);
	}
}
```

```java:BufferedServletInputStream.java
package test;

import java.io.ByteArrayInputStream;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;

public class BufferedServletInputStream extends ServletInputStream {

	private ByteArrayInputStream inputStream;

	public BufferedServletInputStream(byte[] buffer) {
		this.inputStream = new ByteArrayInputStream( buffer );
	}

	@Override
	public int available() {
		return inputStream.available();
	}

	@Override
	public int read() {
		return inputStream.read();
	}

	@Override
	public int read(byte[] b, int off, int len) {
		return inputStream.read( b, off, len );
	}

	@Override
	public boolean isFinished() {
		return available() == 0;
	}

	@Override
	public boolean isReady() {
		return true;
	}

	@Override
	public void setReadListener(ReadListener listener) {
		throw new RuntimeException("Not implemented");
	}
}
```

# 使用例
下記では`doPost()`内で元のリクエストを`BufferedServletRequestWrapper`でラップしていますが、元記事[^2]のように`doFilter()`内でラップしても大丈夫。

```java:TestServlet.java
package test;

import java.util.List;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

@SuppressWarnings("serial")
@WebServlet(name="Test Servlet", urlPatterns={"/*"})
public class TestServlet extends HttpServlet {

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
		try {
			// requestのinputStreamを複数回使えるようにする。
			req = new BufferedServletRequestWrapper(req);

			// multipart/form-dataのパラメータを読み取る。
			FileItemFactory factory = new DiskFileItemFactory();
			ServletFileUpload upload = new ServletFileUpload( factory );
			List<FileItem> uploadItems = upload.parseRequest(req);
			for( FileItem uploadItem : uploadItems ) {
				if(	true
					// && uploadItem.isFormField()		// パラメータのuploadFileを除外したいならコメントアウトを外してみて
						) {
					String fieldName = uploadItem.getFieldName();
					String value = uploadItem.getString();
					System.out.println("fieldName:" + fieldName + " value:" + value);
				}
			}

			// inputStreamをbodyに出力
			// inputStream 2回目の使用だが読み込める・・・！
			req.getInputStream().transferTo(resp.getOutputStream());
		} catch (Exception e) {
			System.out.println(e.getStackTrace());
		}
	}
}
```

# 検証環境
* Java 11
* tomcat 8.5.59
* javax.servlet-api 3.1.0

# 参考
[^1]: Servlet3.0のアップロードで、同時に送られてたパラメータを取得する方法
  https://qiita.com/alpha_pz/items/fe808da6d232a14ac7a9
[^2]: 山奥通信 増刊号 ServletInputStreamを2回使う
  https://yamaokuno-usausa.hatenablog.com/entry/20090120/1232420325
[^3]: multipart/form-dataのパラメータをApache Commons FileUploadで読み取る。
  https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1497572829
[^4]: HttpServletRequestWrapper, example implementation for setReadListener / isFinished / isReady?
  https://stackoverflow.com/questions/29208456/httpservletrequestwrapper-example-implementation-for-setreadlistener-isfinish
[^5]: Http Servlet request lose params from POST body after read it once
  https://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once/17129256