File Uploads with JAX-RS 2
Thursday, May 01, 2014 |If you search for how to upload a file to a JAX-RS 2 endpoint, most suggestions will point you to implementation-specific approaches. While that works, it defeats one of the purposes of a spec: portability. There are some posts out there that will point you in the right direction, though. What I’ll do here, then, is present a clear, portable solution to the problem.
In this example, we’re going to upload arbitrary, binary data. Let’s think of this in HTML terms: we have a form on a page that has a number of text input fields, and at least one file field. In this example, we’ll use two fields: name
, and attachment
. A Java model (which will become more important later), might look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Example {
private String name;
private byte[] attachment;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getAttachment() {
return attachment;
}
public void setAttachment(byte[] attachment) {
this.attachment = attachment;
}
}
One way of getting the data passed to a JAX-RS resource would be to use @FormParam
. Normally, this will work fine, but since we’re wanting a file to be part of the payload, the request must be of type multipart/form-data
, which @FormParam
doesn’t seem to like. Fortunately, the Servlet 3 spec provides an implementation-independent way of dealing with multipart requests: javax.servlet.http.Part
, which we’ll use here. First, the resource method itself:
1
2
3
4
5
6
7
8
9
10
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response formPost(@Context HttpServletRequest request) {
MultipartRequestMap map = new MultipartRequestMap(request);
Example example = new Example();
example.setName(map.getStringParameter("name"));
example.setAttachment(readFile(map.getFileParameter("attachment")));
return Response.ok(buildMessage(example.getName(), example.getAttachment().length)).build();
}
Before looking at where things actually get done, just a quick note here. We are asking the JAX-RS runtime to inject the HttpServletRequest
, which we pass to MultipartRequestMap
(see below). We then pull the fields we want from our Map
, build a model object that we don’t do much with, then return a simple String
response to show that we did something. Pretty simple.
And now, the details:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class MultipartRequestMap extends HashMap<String, List<Object>> {
private static final String DEFAULT_ENCODING = "UTF-8";
private String encoding;
private String tempLocation;
public MultipartRequestMap(HttpServletRequest request) {
this(request, System.getProperty("java.io.tmpdir"));
}
public MultipartRequestMap(HttpServletRequest request, String tempLocation) {
super();
try {
this.tempLocation = tempLocation;
this.encoding = request.getCharacterEncoding();
if (this.encoding == null) {
try {
request.setCharacterEncoding(this.encoding = DEFAULT_ENCODING);
} catch (UnsupportedEncodingException ex) {
Logger.getLogger(MultipartRequestMap.class.getName()).log(Level.SEVERE, null, ex);
}
}
for (Part part : request.getParts()) {
String fileName = part.getSubmittedFileName();
if (fileName == null) {
putMulti(part.getName(), getValue(part));
} else {
processFilePart(part, fileName);
}
}
} catch (IOException | ServletException ex) {
Logger.getLogger(MultipartRequestMap.class.getName()).log(Level.SEVERE, null, ex);
}
}
public String getStringParameter(String name) {
List<Object> list = get(name);
return (list != null) ? (String) get(name).get(0) : null;
}
public File getFileParameter(String name) {
List<Object> list = get(name);
return (list != null) ? (File) get(name).get(0) : null;
}
private void processFilePart(Part part, String fileName) throws IOException {
File tempFile = new File(tempLocation, fileName);
tempFile.createNewFile();
tempFile.deleteOnExit();
try (BufferedInputStream input = new BufferedInputStream(part.getInputStream(), 8192);
BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(tempFile), 8192);) {
byte[] buffer = new byte[8192];
for (int length = 0; ((length = input.read(buffer)) > 0);) {
output.write(buffer, 0, length);
}
} catch (Exception e) {
e.printStackTrace();
}
part.delete();
putMulti(part.getName(), tempFile);
}
private String getValue(Part part) throws IOException {
BufferedReader reader
= new BufferedReader(new InputStreamReader(part.getInputStream(), encoding));
StringBuilder value = new StringBuilder();
char[] buffer = new char[8192];
for (int length; (length = reader.read(buffer)) > 0;) {
value.append(buffer, 0, length);
}
return value.toString();
}
private <T> void putMulti(final String key, final T value) {
List<Object> values = (List<Object>) super.get(key);
if (values == null) {
values = new ArrayList<>();
values.add(value);
put(key, values);
} else {
values.add(value);
}
}
}
This class is based on one by BalusC, though I’ve simplified it some (e.g., removing any EL concerns), so his very well may be more robust. This works well enough, though, for demonstration purposes.
The most interesting part (no pun intended :) is in this loop: for (Part part : request.getParts()) {
. In a nutshell, we’re looping though each Part
returned by the server. If the Part
has a file name, we assume (!!!) it’s a binary part, so we handle it accordingly. Otherwise, we’ll store the value as a simple String
. Note that a key might be given more than once in a request, so we store the values for each key in a List
. This Map
implementation, though, provides convenience methods to get the first value in the List
, which is what we’re interested in. If you’re curious about how the binary data is read off the request, look at processFilePart
.
If you deploy the application now, you’ll get an error at runtime because you need to configure multipart support. It’s a bit obnoxious that there aren’t sensible defaults, but that’s the way it is. In this example, we don’t have any other configuration requirements, we’ll just use the JAX-RS standard application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<multipart-config>
<location>/tmp</location>
<max-file-size>35000000</max-file-size>
<max-request-size>218018841</max-request-size>
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
The area of interest is the <multipart-config>
element. Feel free to tweak the values as you see fit. It might be possible to use annotations (e.g., @ApplicationPath
, @MultipartConfig
, etc) to register all of this without the deployment descriptor, but I haven’t figured out the correct incantation yet, so I use web.xml
. :)
We’re now ready to deploy and test, which we’ll do using curl:
1
2
3
4
5
$ curl -X POST -H 'Accept: application/json' \
-F 'name=Form Upload Example' \
-F 'attachment=@src/main/resources/java.jpg' \
http://localhost:8080/upload-1.0-SNAPSHOT/upload
You uploaded an Example named 'Form Upload Example' with an attachment that is 9425 bytes long.
And there it is! POSTing a binary file to a JAX-RS resource. As I mentioned earlier, there is another, perhaps better way. If you’re using "real" models, there’s no extra magic required:
1
2
3
4
5
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response jsonPost(Example example) {
return Response.ok(buildMessage(example.getName(), example.getAttachment().length)).build();
}
which can be called with:
1
2
3
4
curl -X POST -H 'Content-type: application/json' \
-H 'Accept: application/json' \
-d '{"attachment":"binary data here","name":"JSON Example"}' \
http://localhost:8080/upload-1.0-SNAPSHOT/upload
For this method, JAX-RS (possibly Jersey. I haven’t tested that.) unmarshalls the JSON for us, building the Example
instance, and calling the resource method. It’s much easier and cleaner, so if you can go that route, I’d certainly recommend it, but that’s not always possible. Now, though, you should be equipped to do it either way.