1
15
16 package software.amazon.awssdk.utils.http;
17
18 import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
19
20 import java.io.UnsupportedEncodingException;
21 import java.net.URI;
22 import java.net.URLDecoder;
23 import java.net.URLEncoder;
24 import java.util.Collections;
25 import java.util.LinkedHashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.Optional;
30 import java.util.Set;
31 import java.util.function.UnaryOperator;
32 import java.util.stream.Collectors;
33 import java.util.stream.Stream;
34 import software.amazon.awssdk.annotations.SdkProtectedApi;
35 import software.amazon.awssdk.utils.StringUtils;
36 import software.amazon.awssdk.utils.Validate;
37
38
41 @SdkProtectedApi
42 public final class SdkHttpUtils {
43 private static final String DEFAULT_ENCODING = "UTF-8";
44
45
48 private static final String[] ENCODED_CHARACTERS_WITH_SLASHES = new String[] {"+", "*", "%7E", "%2F"};
49 private static final String[] ENCODED_CHARACTERS_WITH_SLASHES_REPLACEMENTS = new String[] {"%20", "%2A", "~", "/"};
50
51 private static final String[] ENCODED_CHARACTERS_WITHOUT_SLASHES = new String[] {"+", "*", "%7E"};
52 private static final String[] ENCODED_CHARACTERS_WITHOUT_SLASHES_REPLACEMENTS = new String[] {"%20", "%2A", "~"};
53
54
55
56
57 private static final Set<String> SINGLE_HEADERS = Stream.of("age", "authorization",
58 "content-length", "content-location", "content-md5", "content-range", "content-type",
59 "date", "etag", "expires", "from", "host", "if-modified-since", "if-range",
60 "if-unmodified-since", "last-modified", "location", "max-forwards",
61 "proxy-authorization", "range", "referer", "retry-after", "server", "user-agent")
62 .collect(Collectors.toSet());
63
64 private SdkHttpUtils() {
65 }
66
67
70 public static String urlEncode(String value) {
71 return urlEncode(value, false);
72 }
73
74
78 public static String urlEncodeIgnoreSlashes(String value) {
79 return urlEncode(value, true);
80 }
81
82
85 public static String formDataEncode(String value) {
86 return value == null ? null : invokeSafely(() -> URLEncoder.encode(value, DEFAULT_ENCODING));
87 }
88
89
97 public static String urlDecode(String value) {
98 if (value == null) {
99 return null;
100 }
101 try {
102 return URLDecoder.decode(value, DEFAULT_ENCODING);
103 } catch (UnsupportedEncodingException e) {
104 throw new RuntimeException("Unable to decode value", e);
105 }
106 }
107
108
111 public static Map<String, List<String>> encodeQueryParameters(Map<String, List<String>> rawQueryParameters) {
112 return encodeMapOfLists(rawQueryParameters, SdkHttpUtils::urlEncode);
113 }
114
115
118 public static Map<String, List<String>> encodeFormData(Map<String, List<String>> rawFormData) {
119 return encodeMapOfLists(rawFormData, SdkHttpUtils::formDataEncode);
120 }
121
122 private static Map<String, List<String>> encodeMapOfLists(Map<String, List<String>> map, UnaryOperator<String> encoder) {
123 Validate.notNull(map, "Map must not be null.");
124
125 Map<String, List<String>> result = new LinkedHashMap<>();
126
127 for (Entry<String, List<String>> queryParameter : map.entrySet()) {
128 String key = queryParameter.getKey();
129 String encodedKey = encoder.apply(key);
130
131 List<String> value = queryParameter.getValue();
132 List<String> encodedValue = value == null
133 ? null
134 : queryParameter.getValue().stream().map(encoder).collect(Collectors.toList());
135
136 result.put(encodedKey, encodedValue);
137 }
138
139 return result;
140 }
141
142
153 private static String urlEncode(String value, boolean ignoreSlashes) {
154 if (value == null) {
155 return null;
156 }
157
158 String encoded = invokeSafely(() -> URLEncoder.encode(value, DEFAULT_ENCODING));
159
160 if (!ignoreSlashes) {
161 return StringUtils.replaceEach(encoded,
162 ENCODED_CHARACTERS_WITHOUT_SLASHES,
163 ENCODED_CHARACTERS_WITHOUT_SLASHES_REPLACEMENTS);
164 }
165
166 return StringUtils.replaceEach(encoded, ENCODED_CHARACTERS_WITH_SLASHES, ENCODED_CHARACTERS_WITH_SLASHES_REPLACEMENTS);
167 }
168
169
173 public static Optional<String> encodeAndFlattenQueryParameters(Map<String, List<String>> rawQueryParameters) {
174 return flattenQueryParameters(encodeQueryParameters(rawQueryParameters));
175 }
176
177
181 public static Optional<String> encodeAndFlattenFormData(Map<String, List<String>> rawFormData) {
182 return flattenQueryParameters(encodeFormData(rawFormData));
183 }
184
185
189 public static Optional<String> flattenQueryParameters(Map<String, List<String>> toFlatten) {
190 if (toFlatten.isEmpty()) {
191 return Optional.empty();
192 }
193
194 StringBuilder result = new StringBuilder();
195
196 for (Entry<String, List<String>> encodedQueryParameter : toFlatten.entrySet()) {
197 String key = encodedQueryParameter.getKey();
198
199 List<String> values = Optional.ofNullable(encodedQueryParameter.getValue()).orElseGet(Collections::emptyList);
200
201 for (String value : values) {
202 if (result.length() > 0) {
203 result.append('&');
204 }
205 result.append(key);
206 if (value != null) {
207 result.append('=');
208 result.append(value);
209 }
210 }
211 }
212 return Optional.of(result.toString());
213 }
214
215
222 public static boolean isUsingStandardPort(String protocol, Integer port) {
223 Validate.paramNotNull(protocol, "protocol");
224 Validate.isTrue(protocol.equals("http") || protocol.equals("https"),
225 "Protocol must be 'http' or 'https', but was '%s'.", protocol);
226
227 String scheme = StringUtils.lowerCase(protocol);
228
229 return port == null || port == -1 ||
230 (scheme.equals("http") && port == 80) ||
231 (scheme.equals("https") && port == 443);
232 }
233
234
237 public static int standardPort(String protocol) {
238 if (protocol.equalsIgnoreCase("http")) {
239 return 80;
240 } else if (protocol.equalsIgnoreCase("https")) {
241 return 443;
242 } else {
243 throw new IllegalArgumentException("Unknown protocol: " + protocol);
244 }
245 }
246
247
251 public static String appendUri(String baseUri, String path) {
252 Validate.paramNotNull(baseUri, "baseUri");
253 StringBuilder resultUri = new StringBuilder(baseUri);
254
255 if (!StringUtils.isEmpty(path)) {
256 if (!baseUri.endsWith("/")) {
257 resultUri.append("/");
258 }
259
260 resultUri.append(path.startsWith("/") ? path.substring(1) : path);
261 }
262
263 return resultUri.toString();
264 }
265
266
273 public static Stream<String> allMatchingHeaders(Map<String, List<String>> headers, String header) {
274 return headers.entrySet().stream()
275 .filter(e -> e.getKey().equalsIgnoreCase(header))
276 .flatMap(e -> e.getValue() != null ? e.getValue().stream() : Stream.empty());
277 }
278
279
289 public static Optional<String> firstMatchingHeader(Map<String, List<String>> headers, String header) {
290 return allMatchingHeaders(headers, header).findFirst();
291 }
292
293 public static boolean isSingleHeader(String h) {
294 return SINGLE_HEADERS.contains(StringUtils.lowerCase(h));
295 }
296 }
297