· 8 years ago · Dec 07, 2017, 04:26 PM
1package com.sombrainc.zappysales.amazon;
2
3import com.google.common.collect.ImmutableSortedMap;
4import org.apache.commons.io.IOUtils;
5import org.apache.http.HttpResponse;
6import org.apache.http.client.methods.HttpGet;
7import org.apache.http.client.methods.HttpPost;
8import org.apache.http.client.utils.URIBuilder;
9import org.slf4j.Logger;
10import org.slf4j.LoggerFactory;
11
12import javax.crypto.Mac;
13import javax.crypto.spec.SecretKeySpec;
14import java.io.IOException;
15import java.io.StringWriter;
16import java.io.UncheckedIOException;
17import java.io.UnsupportedEncodingException;
18import java.net.URI;
19import java.net.URISyntaxException;
20import java.net.URLEncoder;
21import java.security.InvalidKeyException;
22import java.security.NoSuchAlgorithmException;
23import java.time.LocalDateTime;
24import java.time.ZoneId;
25import java.time.format.DateTimeFormatter;
26import java.util.*;
27
28/* some docs
29 https://developer.amazonservices.com/gp/mws/docs.html
30 http://docs.developer.amazonservices.com/en_US/products/index.html
31 http://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemLookup.html
32*/
33
34public abstract class AmazonRequestBuilder {
35
36 private static final Logger LOGGER = LoggerFactory.getLogger(AmazonRequestBuilder.class);
37
38 private static final String CHARACTER_ENCODING = "UTF-8";
39 protected static final String ALGORITHM = "HmacSHA256";
40 public static final String HTTP_GET = HttpGet.METHOD_NAME;
41 public static final String HTTP_POST = HttpPost.METHOD_NAME;
42
43 private final SortedMap<String, String> parameters = new TreeMap<>();
44 private static DateTimeFormatter iso8601UtcDateFormat;
45
46 private String api_endpoint;
47 private String uri;
48 private String serviceURL;
49 private String secretKey;
50
51 public AmazonRequestBuilder(String api_endpoint, String uri, String secretKey) {
52 this.api_endpoint = api_endpoint;
53 this.uri = uri;
54 this.serviceURL = "https://" + api_endpoint + uri;
55 this.secretKey = secretKey;
56 }
57
58 public static DateTimeFormatter getIso8601UtcDateFormat() {
59 if (iso8601UtcDateFormat == null)
60 iso8601UtcDateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
61 return iso8601UtcDateFormat;
62 }
63
64 public URI buildURI(String httpMethod) {
65 parameters.putAll(getCommonParams());
66 parameters.put("Timestamp", getIso8601UtcDateFormat().format(LocalDateTime.now(ZoneId.of("UTC"))));
67 String formattedParameters = calculateStringToSignV2(httpMethod);
68 String signature = sign(formattedParameters, secretKey);
69 parameters.put("Signature", signature);
70
71 URIBuilder builder = new URIBuilder();
72 builder.setScheme("https")
73 .setHost(api_endpoint)
74 .setPath(uri);
75 parameters.entrySet().forEach(entry -> builder.setParameter(entry.getKey(), entry.getValue()));
76 try {
77 return builder.build();
78 } catch (URISyntaxException e) {
79 throw new RuntimeException(e);
80 }
81 }
82
83
84 /* If Signature Version is 2, string to sign is based on following:
85 *
86 * 1. The HTTP Request Method followed by an ASCII newline (%0A)
87 *
88 * 2. The HTTP Host header in the form of lowercase host,
89 * followed by an ASCII newline.
90 *
91 * 3. The URL encoded HTTP absolute path component of the URI
92 * (up to but not including the query string parameters);
93 * if this is empty use a forward '/'. This parameter is followed
94 * by an ASCII newline.
95 *
96 * 4. The concatenation of all query string components (names and
97 * values) as UTF-8 characters which are URL encoded as per RFC
98 * 3986 (hex characters MUST be uppercase), sorted using
99 * lexicographic byte ordering. Parameter names are separated from
100 * their values by the '=' character (ASCII character 61), even if
101 * the value is empty. Pairs of parameter and values are separated
102 * by the '&' character (ASCII code 38).
103 *
104 */
105 private String calculateStringToSignV2(String requestMethod) {
106 URI endpoint;
107 try {
108 endpoint = new URI(serviceURL.toLowerCase());
109 } catch (URISyntaxException e) {
110 throw new RuntimeException(e);
111 }
112 // Create flattened (String) representation
113 StringBuilder data = new StringBuilder();
114 data.append(requestMethod).append("\n");
115 data.append(endpoint.getHost()).append("\n");
116 data.append(uri).append("\n");
117 Iterator<Map.Entry<String, String>> pairs = parameters.entrySet().iterator();
118 while (pairs.hasNext()) {
119 Map.Entry<String, String> pair = pairs.next();
120 if (pair.getValue() != null) {
121 data.append(urlEncode(pair.getKey()))
122 .append("=")
123 .append(urlEncode(pair.getValue()));
124 } else {
125 data.append(urlEncode(pair.getKey()))
126 .append("=");
127 }
128
129 // Delimit parameters with ampersand (&)
130 if (pairs.hasNext()) {
131 data.append("&");
132 }
133 }
134 return data.toString();
135 }
136
137/*
138 * Sign the text with the given secret key and convert to base64
139 */
140 private static String sign(String data, String secretKey) {
141 try {
142 Mac mac = Mac.getInstance(ALGORITHM);
143 mac.init(new SecretKeySpec(secretKey.getBytes(CHARACTER_ENCODING), ALGORITHM));
144 byte[] signature = mac.doFinal(data.getBytes(CHARACTER_ENCODING));
145 return new String(Base64.getEncoder().encode(signature), CHARACTER_ENCODING);
146 } catch (NoSuchAlgorithmException | UnsupportedEncodingException | InvalidKeyException e) {
147 throw new RuntimeException(e);
148 }
149 }
150
151 private static String urlEncode(String rawValue) {
152 String value = (rawValue == null) ? "" : rawValue;
153 try {
154 return URLEncoder.encode(value, CHARACTER_ENCODING)
155 .replace("+", "%20")
156 .replace("*", "%2A")
157 .replace("%7E", "~");
158 } catch (UnsupportedEncodingException e) {
159 throw new RuntimeException("Unknown encoding: " + CHARACTER_ENCODING, e);
160 }
161 }
162
163 public static String getHttpResponseAsString(HttpResponse response) {
164 try {
165 StringWriter writer = new StringWriter();
166 IOUtils.copy(response.getEntity().getContent(), writer, "UTF-8");
167 return writer.toString();
168 } catch (IOException e) {
169 throw new UncheckedIOException(e);
170 }
171 }
172
173 public void addParameters(Map<String, String> params) {
174 this.parameters.putAll(params);
175 }
176
177 public String addParameter(String key, String value) {
178 return this.parameters.put(key, value);
179 }
180
181
182 protected abstract ImmutableSortedMap<String, String> getCommonParams();
183}