読者です 読者をやめる 読者になる 読者になる

ひらおかゆみのなげやりブログ

もう、なげやりです…

JAX-RSでファイルアップロード!

JavaEE Advent Calendar 2012、17日目の記事です。昨日は@akirakoyasuさんの「Programmiatic CDI」です。

 

Servletではおなじみのファイルアップロード、実はJAX-RSでも実現できることをご存知ですか?

JAX-RSの仕様には含まれていませんが、JAX-RSクライアント同様、多くの実装がファイルアップロード(正確にはマルチパート・リクエスト)に対応しています。私が確認しただけでもJerseyRESTEasyApache CXFがファイルアップロードに対応しています。

実はGoogleで「JAX-RS Multipart」と検索すると47,000件以上もHitします。結構知られているのですね。やり方も1通りではないようです。今回はJerseyとRESTEasyを使った最も簡単な方法についてご紹介しようと思います。

 

§1. Jersey

JerseyはGlassFishWebLogicで採用されている実装で、JAX-RSの参照実装でもあります。今回はGlassFish 3.1.2.2に含まれているJersey 1.11を使用しました。

まずはアップロード画面のHTMLです。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>File Uploader</title>
</head>
<body>
<h1>File Uploader</h1>
<form method="post" action="/JerseyUploader/service/upload" 
 enctype="multipart/form-data">
<p>File: <input type="file" name="file"/></p>
<p><input type="submit" name="send" value="Send"/></p>
</form>
</body>
</html>

続いてメインのソースコードです。

package org.yumix.jerseyuploader;

import static java.nio.file.StandardCopyOption.*;

import java.io.*;
import java.nio.file.*;

import javax.ws.rs.*;

import com.sun.jersey.core.header.*;
import com.sun.jersey.multipart.*;

@Path("/upload")
public class FileUploaderResource {
  @POST
  @Consumes("multipart/form-data")
  @Produces("text/plain")
  public String upload(@FormDataParam("file") InputStream file, 
                       @FormDataParam("file") FormDataContentDisposition fileDisposition, 
                       @FormDataParam("send") String send) {
    // Change following path for the runtime environment
    final String home = System.getenv("USERPROFILE");
    final String downloads = "Downloads";
    
    try {
      Files.copy(file, Paths.get(home, downloads, 
                 fileDisposition.getFileName()), REPLACE_EXISTING);
      return String.format("File [%s] is uploaded", 
                           fileDisposition.getFileName());
    } catch (IOException e) {
      throw new WebApplicationException(e, 500);
    }
  }
}

ネットで調べてみるとMultiPartなんとかというクラスを使ってデータを順番に取り出している例をよく見かけますが、Jerseyでは@FormParamDataというアノテーションを使って、@FormParamと同じ感覚でマルチパート・リクエストを処理できます。結構簡単でしょう?

同じ@FormParamData("file")をInputStreamとFormDataContentDispositionの2ヶ所に付けているのは、InputStreamでファイル本体を読み込み、FormDataContentDispositionでファイル名を取得するためです。

この例では、アップロードされたファイルをサーバ側の所定のフォルダ(Windows 7の「ダウンロード」フォルダ)に保存しています。ここは環境に合わせて書き換えてください。

完全な形のソースコードGitHub(下記URL)にあります。

https://github.com/yumix/JerseyUploader

 

§2. RESTEasy

RESTEasyはJBossに採用されている実装で、書籍「JavaによるRESTfulシステム構築」が多くのページを割いて取り上げているものです。今回はJBoss AS 7.1.1に含まれているRESTEasy 2.3.2を使用しました。

まずはアップロード画面のHTMLです。Jerseyの例で使ったHTMLを使いまわせなかった理由は後でご説明します。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>File Uploader</title>
<script type="text/javascript">
function getFileName(path) {
  return path.replace(/.*[\/\\]([^\/\\]+)$/, "$1");
}
</script>
</head>
<body>
<h1>File Uploader</h1>
<form method="post" action="/RESTEasyUploader/service/upload" 
 enctype="multipart/form-data">
<p>File: 
   <input type="file" name="file" 
    onchange="document.forms[0].filename.value = getFileName(this.value)"/>
   <input type="hidden" name="filename"/></p>
<p><input type="submit" name="send" value="Send"/></p>
</form>
</body>
</html>

続いてメインのソースコードです。RESTEasyの場合はマルチパートのデータを格納するためのクラスを別途用意する必要があります。

package org.yumix.restreasyuploader;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.io.IOException;
import java.nio.file.*;

import javax.ws.rs.*;

import org.jboss.resteasy.annotations.providers.multipart.*;

@Path("/upload")
public class FileUploaderResource {
  // Change following path for the runtime environment
  final String home = System.getenv("USERPROFILE");
  final String downloads = "Downloads";
  
  @POST
  @Consumes("multipart/form-data")
  @Produces("text/plain")
  public String upload(@MultipartForm UploadForm form) {
    try {
      Files.copy(form.getFile(), 
                 Paths.get(home, downloads, form.getFileName()),
                 REPLACE_EXISTING);
      return String.format("File [%s] is uploaded", form.getFileName());
    } catch (IOException e) {
      throw new WebApplicationException(e, 500);
    }
  }
}

package org.yumix.restreasyuploader;

import java.io.*;

import javax.ws.rs.*;

import org.jboss.resteasy.annotations.providers.multipart.*;

public class UploadForm {
  @FormParam("file")
  @PartType("application/octet-stream")
  private InputStream file;
	
  @FormParam("filename")
  @PartType("text/plain")
  private String fileName;
	
  @FormParam("send")
  @PartType("text/plain")
  private String send;
  
  // getters and setters are omitted
}

RESTEasyの場合は@MultipartFormアノテーションを使い、解析後のマルチパート・リクエストのデータを一旦JavaBeanに格納します。JavaBeanの中でリクエストのどの部分を切り出すのか細かく指定できるので、少し手間はかかりますが応用範囲は広いと言えそうです。

ただ残念なことに、RESTEasyの実装はファイル名のハンドリングが難しいようで、特に@MultipartFormを使ったやり方ではファイル名を取得できないようです(私の調べ方が甘いのかな?)。

私はどうしても@MultipartFormで簡単に作りたかったので、HTMLに隠しフィールドを埋め込んで、JavaScriptを使ってファイル選択時にファイル名を隠しフィールドに設定する ようにしました。

完全な形のソースコードGitHub(下記URL)にあります。

https://github.com/yumix/RESTEasyUploader

 

JAX-RSの次期バージョンでは、クライアントが標準仕様に含まれるそうです。ファイルアップロードも早く標準化されるとよいですね。

 

明日は@n_agetsuさんです。ちなみに明後日は私の誕生日です。