Skip to content

Commit 3e56fc3

Browse files
committed
Setting Content-Type header for Streaming request. Related to #357
1 parent 0cdd257 commit 3e56fc3

File tree

13 files changed

+1319
-619
lines changed

13 files changed

+1319
-619
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"type": "feature",
4+
"description": "Setting `Content-Type` header for streaming requests. Related to [#357](https://github.com/aws/aws-sdk-java-v2/issues/357)"
5+
}

codegen/src/main/resources/templates/rest-xml/request-marshaller.ftl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,6 @@ public class ${shapeName}Marshaller implements Marshaller<Request<${shapeName}>,
7575
<#list shape.members as member>
7676
<#if (member.http.isStreaming)>
7777
<#-- Content is set by StreamingRequestMarshaller -->
78-
if (!request.getHeaders().containsKey("Content-Type")) {
79-
request.addHeader("Content-Type", "binary/octet-stream");
80-
}
8178
<#elseif (member.http.isPayload) && member.variable.variableType = "java.nio.ByteBuffer">
8279
request.setContent(BinaryUtils.toStream(${shape.variable.variableName}.${member.fluentGetterMethodName}()));
8380
if (!request.getHeaders().containsKey("Content-Type")) {

core/src/main/java/software/amazon/awssdk/core/protocol/json/internal/JsonProtocolMarshaller.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515

1616
package software.amazon.awssdk.core.protocol.json.internal;
1717

18+
import static software.amazon.awssdk.http.Headers.CONTENT_LENGTH;
19+
import static software.amazon.awssdk.http.Headers.CONTENT_TYPE;
20+
import static software.amazon.awssdk.utils.StringUtils.isNotBlank;
21+
1822
import java.io.ByteArrayInputStream;
1923
import java.io.InputStream;
2024
import java.nio.ByteBuffer;
@@ -180,11 +184,11 @@ public Request<OrigRequestT> finishMarshalling() {
180184
byte[] content = jsonGenerator.getBytes();
181185
request.setContent(new ByteArrayInputStream(content));
182186
if (content.length > 0) {
183-
request.addHeader("Content-Length", Integer.toString(content.length));
187+
request.addHeader(CONTENT_LENGTH, Integer.toString(content.length));
184188
}
185189
}
186-
if (!request.getHeaders().containsKey("Content-Type")) {
187-
request.addHeader("Content-Type", contentType);
190+
if (!request.getHeaders().containsKey(CONTENT_TYPE) && isNotBlank(contentType)) {
191+
request.addHeader(CONTENT_TYPE, contentType);
188192
}
189193
return request;
190194
}

core/src/main/java/software/amazon/awssdk/core/runtime/transform/StreamingRequestMarshaller.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.core.runtime.transform;
1717

1818
import static software.amazon.awssdk.http.Headers.CONTENT_LENGTH;
19+
import static software.amazon.awssdk.http.Headers.CONTENT_TYPE;
1920
import static software.amazon.awssdk.utils.Validate.paramNotNull;
2021

2122
import software.amazon.awssdk.annotations.SdkProtectedApi;
@@ -46,7 +47,11 @@ public StreamingRequestMarshaller(Marshaller<Request<T>, T> delegate, RequestBod
4647
public Request<T> marshall(T in) {
4748
Request<T> marshalled = delegate.marshall(in);
4849
marshalled.setContent(requestBody.asStream());
49-
marshalled.addHeader(CONTENT_LENGTH, String.valueOf(requestBody.getContentLength()));
50+
if (!marshalled.getHeaders().containsKey(CONTENT_TYPE)) {
51+
marshalled.addHeader(CONTENT_TYPE, requestBody.contentType());
52+
}
53+
54+
marshalled.addHeader(CONTENT_LENGTH, String.valueOf(requestBody.contentLength()));
5055
return marshalled;
5156
}
5257
}

core/src/main/java/software/amazon/awssdk/core/sync/RequestBody.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.nio.charset.StandardCharsets;
2828
import java.nio.file.Path;
2929
import java.util.Arrays;
30+
import software.amazon.awssdk.core.util.Mimetypes;
3031
import software.amazon.awssdk.utils.BinaryUtils;
3132

3233
/**
@@ -40,10 +41,12 @@ public class RequestBody {
4041
// TODO Handle stream management (progress listener, orig input stream tracking, etc
4142
private final InputStream inputStream;
4243
private final long contentLength;
44+
private final String contentType;
4345

44-
private RequestBody(InputStream inputStream, long contentLength) {
46+
private RequestBody(InputStream inputStream, long contentLength, String contentType) {
4547
this.inputStream = paramNotNull(inputStream, "contents");
4648
this.contentLength = contentLength;
49+
this.contentType = paramNotNull(contentType, "contentType");
4750
validState(contentLength >= 0, "Content length must be greater than or equal to zero");
4851
}
4952

@@ -57,10 +60,17 @@ public InputStream asStream() {
5760
/**
5861
* @return Content length of {@link RequestBody}.
5962
*/
60-
public long getContentLength() {
63+
public long contentLength() {
6164
return contentLength;
6265
}
6366

67+
/**
68+
* @return Content type of {@link RequestBody}.
69+
*/
70+
public String contentType() {
71+
return contentType;
72+
}
73+
6474
/**
6575
* Create a {@link RequestBody} using the full contents of the specified file.
6676
*
@@ -78,7 +88,9 @@ public static RequestBody of(Path path) {
7888
* @return RequestBody instance.
7989
*/
8090
public static RequestBody of(File file) {
81-
return new RequestBody(invokeSafely(() -> new FileInputStream(file)), file.length());
91+
return new RequestBody(invokeSafely(() -> new FileInputStream(file)),
92+
file.length(),
93+
Mimetypes.getInstance().getMimetype(file));
8294
}
8395

8496
/**
@@ -94,7 +106,7 @@ public static RequestBody of(File file) {
94106
* @return RequestBody instance.
95107
*/
96108
public static RequestBody of(InputStream inputStream, long contentLength) {
97-
return new RequestBody(inputStream, contentLength);
109+
return new RequestBody(inputStream, contentLength, Mimetypes.MIMETYPE_OCTET_STREAM);
98110
}
99111

100112
/**
@@ -104,7 +116,7 @@ public static RequestBody of(InputStream inputStream, long contentLength) {
104116
* @return RequestBody instance.
105117
*/
106118
public static RequestBody of(String contents) {
107-
return RequestBody.of(contents.getBytes(StandardCharsets.UTF_8));
119+
return ofByteDirect(contents.getBytes(StandardCharsets.UTF_8), Mimetypes.MIMETYPE_TEXT_PLAIN);
108120
}
109121

110122
/**
@@ -142,7 +154,14 @@ public static RequestBody empty() {
142154
* Creates a {@link RequestBody} using the specified bytes (without copying).
143155
*/
144156
private static RequestBody ofByteDirect(byte[] bytes) {
145-
return new RequestBody(new ByteArrayInputStream(bytes), bytes.length);
157+
return ofByteDirect(bytes, Mimetypes.MIMETYPE_OCTET_STREAM);
158+
}
159+
160+
/**
161+
* Creates a {@link RequestBody} using the specified bytes (without copying).
162+
*/
163+
private static RequestBody ofByteDirect(byte[] bytes, String mimetype) {
164+
return new RequestBody(new ByteArrayInputStream(bytes), bytes.length, mimetype);
146165
}
147166

148167
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.util;
17+
18+
import java.io.BufferedReader;
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.io.InputStreamReader;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import java.util.Optional;
26+
import java.util.StringTokenizer;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
import software.amazon.awssdk.utils.IoUtils;
30+
31+
/**
32+
* Utility class that maintains a listing of known Mimetypes, and determines the
33+
* mimetype of files based on file extensions.
34+
* <p>
35+
* This class is obtained with the {#link {@link #getInstance()} method that
36+
* recognizes loaded mime types from the file <code>mime.types</code> if this
37+
* file is available at the root of the classpath. The mime.types file format,
38+
* and most of the content, is taken from the Apache HTTP server's mime.types
39+
* file.
40+
* <p>
41+
* The format for mime type setting documents is:
42+
* <code>mimetype + extension (+ extension)*</code>. Any
43+
* blank lines in the file are ignored, as are lines starting with
44+
* <code>#</code> which are considered comments.
45+
*
46+
* @see <a href="https://github.com/apache/httpd/blob/trunk/docs/conf/mime.types">mime.types</a>
47+
*/
48+
public final class Mimetypes {
49+
50+
/** The default XML mimetype: application/xml */
51+
public static final String MIMETYPE_XML = "application/xml";
52+
53+
/** The default HTML mimetype: text/html */
54+
public static final String MIMETYPE_HTML = "text/html";
55+
56+
/** The default binary mimetype: application/octet-stream */
57+
public static final String MIMETYPE_OCTET_STREAM = "application/octet-stream";
58+
59+
/** The default gzip mimetype: application/x-gzip */
60+
public static final String MIMETYPE_GZIP = "application/x-gzip";
61+
62+
public static final String MIMETYPE_TEXT_PLAIN = "text/plain; charset=UTF-8";
63+
64+
private static final Logger LOG = LoggerFactory.getLogger(Mimetypes.class);
65+
66+
private static final String MIME_TYPE_PATH = "software/amazon/awssdk/core/util/mime.types";
67+
68+
private static final ClassLoader CLASS_LOADER = ClassLoaderHelper.classLoader();
69+
70+
private static volatile Mimetypes mimetypes;
71+
72+
/**
73+
* Map that stores file extensions as keys, and the corresponding mimetype as values.
74+
*/
75+
private final Map<String, String> extensionToMimetype = new HashMap<>();
76+
77+
private Mimetypes() {
78+
Optional.ofNullable(CLASS_LOADER).map(loader -> loader.getResourceAsStream(MIME_TYPE_PATH)).ifPresent(
79+
stream -> {
80+
try {
81+
loadAndReplaceMimetypes(stream);
82+
} catch (IOException e) {
83+
LOG.debug("Failed to load mime types from file in the classpath: mime.types", e);
84+
} finally {
85+
IoUtils.closeQuietly(stream, null);
86+
}
87+
}
88+
);
89+
}
90+
91+
/**
92+
* Loads MIME type info from the file 'mime.types' in the classpath, if it's available.
93+
*/
94+
public static Mimetypes getInstance() {
95+
if (mimetypes == null) {
96+
synchronized (Mimetypes.class) {
97+
if (mimetypes == null) {
98+
mimetypes = new Mimetypes();
99+
}
100+
}
101+
}
102+
103+
return mimetypes;
104+
}
105+
106+
/**
107+
* Determines the mimetype of a file by looking up the file's extension in an internal listing
108+
* to find the corresponding mime type. If the file has no extension, or the extension is not
109+
* available in the listing contained in this class, the default mimetype
110+
* <code>application/octet-stream</code> is returned.
111+
* <p>
112+
* A file extension is one or more characters that occur after the last period (.) in the file's name.
113+
* If a file has no extension,
114+
* Guesses the mimetype of file data based on the file's extension.
115+
*
116+
* @param file the file whose extension may match a known mimetype.
117+
* @return the file's mimetype based on its extension, or a default value of
118+
* <code>application/octet-stream</code> if a mime type value cannot be found.
119+
*/
120+
public String getMimetype(File file) {
121+
return getMimetype(file.getName());
122+
}
123+
124+
/**
125+
* Determines the mimetype of a file by looking up the file's extension in
126+
* an internal listing to find the corresponding mime type. If the file has
127+
* no extension, or the extension is not available in the listing contained
128+
* in this class, the default mimetype <code>application/octet-stream</code>
129+
* is returned.
130+
* <p>
131+
* A file extension is one or more characters that occur after the last
132+
* period (.) in the file's name. If a file has no extension, Guesses the
133+
* mimetype of file data based on the file's extension.
134+
*
135+
* @param fileName The name of the file whose extension may match a known
136+
* mimetype.
137+
* @return The file's mimetype based on its extension, or a default value of
138+
* {@link #MIMETYPE_OCTET_STREAM} if a mime type value cannot
139+
* be found.
140+
*/
141+
String getMimetype(String fileName) {
142+
int lastPeriodIndex = fileName.lastIndexOf('.');
143+
if (lastPeriodIndex > 0 && lastPeriodIndex + 1 < fileName.length()) {
144+
String ext = StringUtils.lowerCase(fileName.substring(lastPeriodIndex + 1));
145+
if (extensionToMimetype.containsKey(ext)) {
146+
return extensionToMimetype.get(ext);
147+
}
148+
}
149+
return MIMETYPE_OCTET_STREAM;
150+
}
151+
152+
/**
153+
* Reads and stores the mime type setting corresponding to a file extension, by reading
154+
* text from an InputStream. If a mime type setting already exists when this method is run,
155+
* the mime type value is replaced with the newer one.
156+
*/
157+
private void loadAndReplaceMimetypes(InputStream is) throws IOException {
158+
BufferedReader br = new BufferedReader(new InputStreamReader(is, StringUtils.UTF8));
159+
160+
br.lines().filter(line -> !line.startsWith("#")).forEach(line -> {
161+
line = line.trim();
162+
163+
StringTokenizer st = new StringTokenizer(line, " \t");
164+
if (st.countTokens() > 1) {
165+
String mimetype = st.nextToken();
166+
while (st.hasMoreTokens()) {
167+
String extension = st.nextToken();
168+
extensionToMimetype.put(StringUtils.lowerCase(extension), mimetype);
169+
}
170+
}
171+
});
172+
}
173+
}

0 commit comments

Comments
 (0)