diff --git a/clients/sellingpartner-api-documents-helper-java/.gitignore b/clients/sellingpartner-api-documents-helper-java/.gitignore
new file mode 100644
index 0000000..e3800fd
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/.gitignore
@@ -0,0 +1,22 @@
+.idea
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# exclude jar for gradle wrapper
+!gradle/wrapper/*.jar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# build files
+**/target
+target
+.gradle
+build
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/README.md b/clients/sellingpartner-api-documents-helper-java/README.md
new file mode 100644
index 0000000..2637fc6
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/README.md
@@ -0,0 +1,207 @@
+# Selling Partner API Documents Helper
+
+This library provides helper classes that you can use to work with encrypted documents. Specifically, they help with:
+* Encrypting and uploading a document
+* Downloading an encrypted document and reading its decrypted contents
+
+Note: It’s the developer’s responsibility to always maintain encryption at rest. Unencrypted document content should never be stored on disk, even temporarily, because documents can contain sensitive information.
+The helper classes provided in this library are built to assist with this.
+
+## Example usage
+
+### Upload
+
+```java
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.nio.charset.StandardCharsets;
+
+import com.amazon.spapi.documents.UploadHelper;
+import com.amazon.spapi.documents.UploadSpecification;
+import com.amazon.spapi.documents.exception.CryptoException;
+import com.amazon.spapi.documents.exception.HttpResponseException;
+import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
+
+/* We want to maintain encryption at rest, so do not write unencrypted data to disk. This is bad:
+ InputStream source = new FileInputStream(new File("/path/to/data.txt"));
+
+ Instead, if your data can fit in memory, you can create an InputStream from a String (see encryptAndUpload_fromString()).
+ Otherwise, you can pipe data into an InputStream using Piped streams (see encryptAndUpload_fromPipedInputStream()).
+*/
+public class UploadExample {
+ private final UploadHelper uploadHelper = new UploadHelper.Builder().build();
+
+ public void encryptAndUpload_fromString(String key, String initializationVector, String url) {
+ AESCryptoStreamFactory aesCryptoStreamFactory =
+ new AESCryptoStreamFactory.Builder(key, initializationVector)
+ .build();
+
+ String contentType = String.format("text/plain; charset=%s", StandardCharsets.UTF_8);
+
+ // The character set must be the same one that is specified in contentType.
+ try (InputStream source = new ByteArrayInputStream("my document contents".getBytes(StandardCharsets.UTF_8))) {
+ UploadSpecification uploadSpec =
+ new UploadSpecification.Builder(contentType, aesCryptoStreamFactory, source, url)
+ .build();
+
+ uploadHelper.upload(uploadSpec);
+ } catch (CryptoException | HttpResponseException | IOException e) {
+ // Handle exception.
+ }
+ }
+ public void encryptAndUpload_fromPipedInputStream(String key, String initializationVector, String url) {
+ AESCryptoStreamFactory aesCryptoStreamFactory =
+ new AESCryptoStreamFactory.Builder(key, initializationVector)
+ .build();
+
+ String contentType = String.format("text/plain; charset=%s", StandardCharsets.UTF_8);
+
+ try (PipedInputStream source = new PipedInputStream()) {
+ new Thread (
+ new Runnable() {
+ public void run() {
+ try (PipedOutputStream documentContents = new PipedOutputStream(source)) {
+ // The character set must be the same one that is specified in contentType.
+ documentContents.write("my document contents\n".getBytes(StandardCharsets.UTF_8));
+ documentContents.write("more document contents".getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ // Handle exception.
+ }
+ }
+ }
+ ).start();
+
+ UploadSpecification uploadSpec =
+ new UploadSpecification.Builder(contentType, aesCryptoStreamFactory, source, url)
+ .build();
+
+ uploadHelper.upload(uploadSpec);
+ } catch (CryptoException | HttpResponseException | IOException e) {
+ // Handle exception.
+ }
+ }
+}
+```
+
+### Download
+
+```java
+import java.io.BufferedReader;
+import java.io.IOException;
+
+import com.amazon.spapi.documents.CompressionAlgorithm;
+import com.amazon.spapi.documents.DownloadBundle;
+import com.amazon.spapi.documents.DownloadHelper;
+import com.amazon.spapi.documents.DownloadSpecification;
+import com.amazon.spapi.documents.exception.CryptoException;
+import com.amazon.spapi.documents.exception.HttpResponseException;
+import com.amazon.spapi.documents.exception.MissingCharsetException;
+import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
+
+public class DownloadExample {
+ final DownloadHelper downloadHelper = new DownloadHelper.Builder().build();
+
+ public void downloadAndDecrypt(String key, String initializationVector, String url, String compressionAlgorithm) {
+ AESCryptoStreamFactory aesCryptoStreamFactory =
+ new AESCryptoStreamFactory.Builder(key, initializationVector).build();
+
+ DownloadSpecification downloadSpec = new DownloadSpecification.Builder(aesCryptoStreamFactory, url)
+ .withCompressionAlgorithm(CompressionAlgorithm.fromEquivalent(compressionAlgorithm))
+ .build();
+
+ try (DownloadBundle downloadBundle = downloadHelper.download(downloadSpec)) {
+ // This example assumes that the downloaded document has a charset in the content type, e.g.
+ // text/plain; charset=UTF-8
+ try (BufferedReader reader = downloadBundle.newBufferedReader()) {
+ String line;
+ do {
+ line = reader.readLine();
+ // Process the decrypted line.
+ } while (line != null);
+ }
+ } catch (CryptoException | HttpResponseException | IOException | MissingCharsetException e) {
+ // Handle exception here.
+ }
+ }
+}
+```
+
+## Requirements
+
+Building the Selling Partner API Documents Helper requires:
+1. Java 1.8+
+2. Maven/Gradle
+
+
+## Installation
+
+To install the Selling Partner API Documents Helper to your local Maven repository, simply execute:
+
+```shell
+mvn clean install
+```
+
+To deploy it to a remote Maven repository instead, configure the settings of the repository and execute:
+
+```shell
+mvn clean deploy
+```
+
+Refer to the [OSSRH Guide](http://central.sonatype.org/pages/ossrh-guide.html) for more information.
+
+### Maven users
+
+Add this dependency to your project's POM:
+
+```xml
+
+ com.amazon.sellingpartnerapi
+ sellingpartner-api-documents-helper-java
+ 1.0.0
+
+```
+
+### Gradle users
+
+Add this dependency to your project's build file:
+
+```groovy
+implementation "com.amazon.sellingpartnerapi:sellingpartner-api-documents-helper-java:1.0.0"
+```
+
+### Others
+
+At first generate the JAR by executing:
+
+```shell
+mvn clean package
+```
+
+Then manually install the following JARs:
+
+* `target/sellingpartner-api-documents-helper-java.jar`
+* `target/lib/*.jar`
+
+## License
+Swagger Codegen templates are subject to the [Swagger Codegen License](https://github.com/swagger-api/swagger-codegen#license).
+
+All other work licensed as follows:
+
+Copyright Amazon.com Inc. or its affiliates.
+
+All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this library except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/clients/sellingpartner-api-documents-helper-java/build.gradle b/clients/sellingpartner-api-documents-helper-java/build.gradle
new file mode 100644
index 0000000..4f341f9
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/build.gradle
@@ -0,0 +1,73 @@
+plugins {
+ id 'idea'
+ id 'eclipse'
+ id 'java'
+}
+
+group = 'com.amazon.sellingpartnerapi'
+version = '1.0.0'
+
+
+repositories {
+ jcenter()
+ mavenCentral()
+}
+
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
+
+
+/*sourceSets {
+ main {
+ java {
+ srcDirs = ['src/main/java']
+ }
+ resources {
+ srcDirs = ['src/main/resources']
+ }
+ }
+
+ test {
+ java {
+ srcDirs = ['src/test/java']
+ }
+ resources {
+ srcDirs = ['src/test/resources']
+ }
+ }
+}*/
+
+task execute(type: JavaExec) {
+ main = System.getProperty('mainClass')
+ classpath = sourceSets.main.runtimeClasspath
+}
+
+test {
+ useJUnitPlatform()
+ testLogging {
+ events "passed", "skipped", "failed"
+ }
+}
+
+dependencies {
+ testImplementation(platform('org.junit:junit-bom:5.7.0'))
+ testImplementation('org.junit.jupiter:junit-jupiter')
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.7.0")
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0")
+ testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.7.0")
+
+ implementation 'com.squareup.okhttp:okhttp:2.7.5'
+ implementation 'com.google.guava:guava:28.2-jre'
+
+ // https://mvnrepository.com/artifact/org.threeten/threetenbp
+ implementation group: 'org.threeten', name: 'threetenbp', version: '1.3.5'
+
+ // https://mvnrepository.com/artifact/junit/junit
+ implementation 'org.junit.jupiter:junit-jupiter-migrationsupport:5.5.1'
+
+ implementation 'org.mockito:mockito-core:3.0.0'
+ implementation 'org.mockito:mockito-inline:3.0.0'
+
+ implementation 'org.apache.directory.studio:org.apache.commons.io:2.4'
+
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.jar b/clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..87b738c
Binary files /dev/null and b/clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.properties b/clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3ac035e
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Sep 23 13:37:56 CST 2020
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/clients/sellingpartner-api-documents-helper-java/pom.xml b/clients/sellingpartner-api-documents-helper-java/pom.xml
new file mode 100644
index 0000000..1904804
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/pom.xml
@@ -0,0 +1,99 @@
+
+
+ 4.0.0
+ com.amazon.sellingpartnerapi
+ sellingpartner-api-documents-helper-java
+ 1.0.0
+
+
+ com.squareup.okhttp
+ okhttp
+ 2.7.5
+
+
+ com.squareup.okhttp
+ logging-interceptor
+ 2.7.5
+
+
+ io.gsonfire
+ gson-fire
+ 1.8.0
+
+
+ org.apache.directory.studio
+ org.apache.commons.io
+ 2.4
+
+
+ com.google.guava
+ guava
+ 28.2-jre
+
+
+ org.threeten
+ threetenbp
+ 1.3.5
+
+
+
+ org.junit.platform
+ junit-platform-commons
+ 1.7.0
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.0.0
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.3.2
+
+
+ org.junit.jupiter
+ junit-jupiter-migrationsupport
+ 5.5.1
+
+
+ org.mockito
+ mockito-core
+ 3.0.0
+
+
+ org.mockito
+ mockito-inline
+ 3.0.0
+
+
+
+
+
+ maven-surefire-plugin
+ 2.22.2
+
+
+ maven-failsafe-plugin
+ 2.22.2
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.7.0
+
+
+
+
+
+ 1.8
+ ${java.version}
+ ${java.version}
+ 1.0.0
+ UTF-8
+
+
diff --git a/clients/sellingpartner-api-documents-helper-java/settings.gradle b/clients/sellingpartner-api-documents-helper-java/settings.gradle
new file mode 100644
index 0000000..07ac347
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = "sellingpartner-api-documents-helper-java"
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/CompressionAlgorithm.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/CompressionAlgorithm.java
new file mode 100644
index 0000000..a107d2d
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/CompressionAlgorithm.java
@@ -0,0 +1,36 @@
+package com.amazon.spapi.documents;
+
+/**
+ * The compression algorithm.
+ */
+public enum CompressionAlgorithm {
+ GZIP;
+
+ /**
+ * Convert from any equivalent enum value. If the specified enum value is null, return null.
+ *
+ * @param val The equivalent enum value to convert
+ * @return This enum's equivalent to the specified enum value
+ */
+ public static CompressionAlgorithm fromEquivalent(T val) {
+ if (val != null) {
+ return CompressionAlgorithm.valueOf(val.toString());
+ }
+
+ return null;
+ }
+
+ /**
+ * Convert from a string. If the specified string is null, return null.
+ *
+ * @param val The value to convert.
+ * @return This enum's equivalent to the specified string
+ */
+ public static CompressionAlgorithm fromEquivalent(String val) {
+ if (val != null) {
+ return CompressionAlgorithm.valueOf(val);
+ }
+
+ return null;
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/CryptoStreamFactory.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/CryptoStreamFactory.java
new file mode 100644
index 0000000..f8ecaca
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/CryptoStreamFactory.java
@@ -0,0 +1,29 @@
+package com.amazon.spapi.documents;
+
+import com.amazon.spapi.documents.exception.CryptoException;
+
+import java.io.InputStream;
+
+/**
+ * Crypto stream factory interface.
+ */
+public interface CryptoStreamFactory {
+ /**
+ * Create a new {@link InputStream} that decrypts a stream of encrypted data.
+ *
+ * @param source The source for encrypted data to decrypt
+ * @return A new {@link InputStream} from which decrypted data can be read
+ * @throws CryptoException Crypto exception
+ */
+ InputStream newDecryptStream(InputStream source) throws CryptoException;
+
+ /**
+ * Create a new {@link InputStream} that encrypts a stream of unencrypted data.
+ *
+ * @param source The source for unencrypted data to encrypt
+ * @return A new {@link InputStream} from which encrypted data can be read
+ * @throws CryptoException Crypto exception
+ */
+ InputStream newEncryptStream(InputStream source) throws CryptoException;
+
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadBundle.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadBundle.java
new file mode 100644
index 0000000..028f908
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadBundle.java
@@ -0,0 +1,153 @@
+package com.amazon.spapi.documents;
+
+import com.amazon.spapi.documents.exception.CryptoException;
+import com.amazon.spapi.documents.exception.MissingCharsetException;
+import com.squareup.okhttp.MediaType;
+import org.apache.commons.io.IOUtils;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Helper that contains and provides access to the downloaded contents of an encrypted document. {@link #close()} must
+ * be called to delete the temporary file that contains the encrypted document contents.
+ *
+ * Multiple independent streams and readers (from which unencrypted data can be read while maintaining encryption at
+ * rest on the filesystem) can be opened from an instance of {@link DownloadBundle}, but once {@link #close()} is
+ * called the behavior of any open streams or readers from this instance will be unspecified as the underlying
+ * temporary file will be deleted.
+ */
+public class DownloadBundle implements AutoCloseable {
+ private final CompressionAlgorithm compressionAlgorithm;
+ private final String contentType;
+ private final CryptoStreamFactory cryptoStreamFactory;
+ private final File document;
+
+ DownloadBundle(CompressionAlgorithm compressionAlgorithm, String contentType,
+ CryptoStreamFactory cryptoStreamFactory, File document) {
+ this.compressionAlgorithm = compressionAlgorithm;
+ this.contentType = contentType;
+ this.cryptoStreamFactory = cryptoStreamFactory;
+ this.document = document;
+ }
+
+ /**
+ * The content type of the unencrypted document contents.
+ *
+ * @return The content type
+ */
+ public String getContentType() {
+ return contentType;
+ }
+
+ /**
+ * Open an {@link InputStream} that allows the caller to read the decompressed (if applicable) and decrypted
+ * contents of the downloaded document. It is the responsibility of the caller to close the returned
+ * {@link InputStream}. The {@link InputStream}'s operation will be unspecified once this {@link DownloadBundle} is
+ * closed.
+ *
+ * @return An {@link InputStream} that decompresses and decrypts the document's contents
+ * @throws CryptoException Crypto exception
+ * @throws IOException IO exception
+ */
+ public InputStream newInputStream() throws CryptoException, IOException {
+ Closeable closeThis = null;
+ try {
+ InputStream inputStream = new FileInputStream(document);
+ closeThis = inputStream;
+
+ inputStream = cryptoStreamFactory.newDecryptStream(inputStream);
+ closeThis = inputStream;
+
+ if (compressionAlgorithm != null) {
+ switch (compressionAlgorithm) {
+ case GZIP:
+ inputStream = new GZIPInputStream(inputStream);
+ closeThis = inputStream;
+ }
+ }
+
+ closeThis = null;
+ return inputStream;
+ } finally {
+ IOUtils.closeQuietly(closeThis);
+ }
+ }
+
+ /**
+ * Open a {@link BufferedReader} that allows the caller to read the decompressed (if applicable) and decrypted
+ * contents of the downloaded document. The character set is parsed from {@link #getContentType()}. If the character
+ * set could not be parsed, this method will fail with {@link MissingCharsetException}.
+ *
+ * It is the responsibility of the caller to close the returned {@link BufferedReader}. This {@link BufferedReader}
+ * will become invalid once this {@link DownloadBundle} is closed.
+ *
+ * @return A {@link BufferedReader} that decompresses and decrypts the document's contents
+ * @throws CryptoException Crypto exception
+ * @throws IOException IO exception
+ * @throws MissingCharsetException The character set could not be parsed from {@link #getContentType()}
+ */
+ public BufferedReader newBufferedReader() throws CryptoException, IOException, MissingCharsetException {
+ return newBufferedReader(null);
+ }
+
+ /**
+ * Open a {@link BufferedReader} that allows the caller to read the decompressed (if applicable) and decrypted
+ * contents of the downloaded document. The character set is parsed from {@link #getContentType()}. If the character
+ * set could not be parsed and defaultCharset
is specified, this method will attempt to open the reader
+ * with defaultCharset
. Otherwise, this method will fail with {@link MissingCharsetException}.
+ *
+ * It is the responsibility of the caller to close the returned {@link BufferedReader}. This {@link BufferedReader}
+ * will become invalid once this {@link DownloadBundle} is closed.
+ *
+ * @param defaultCharset The default charset to use if a charset cannot be parsed from the content type.
+ * @return A {@link BufferedReader} that decompresses and decrypts the document's contents
+ * @throws CryptoException Crypto exception
+ * @throws IOException IO exception
+ * @throws MissingCharsetException The character set could not be parsed from {@link #getContentType()} and
+ * defaultCharset
was not specified.
+ */
+ public BufferedReader newBufferedReader(Charset defaultCharset) throws CryptoException, IOException,
+ MissingCharsetException {
+ String contentType = getContentType();
+
+ Charset charset = MediaType.parse(contentType).charset();
+ if (charset == null) {
+ charset = defaultCharset;
+ }
+
+ if (charset == null) {
+ throw new MissingCharsetException(String.format(
+ "Could not parse character set from content type '%s' and no default provided", contentType));
+ }
+
+ Closeable closeThis = null;
+ try {
+ InputStream inputStream = newInputStream();
+ closeThis = inputStream;
+
+ InputStreamReader reader = new InputStreamReader(inputStream, charset);
+ closeThis = reader;
+
+ BufferedReader bufferedReader = new BufferedReader(reader);
+ closeThis = null;
+ return bufferedReader;
+ } finally {
+ IOUtils.closeQuietly(closeThis);
+ }
+ }
+
+ /**
+ * Closes this {@link DownloadBundle}, deleting the temporary file containing the encrypted document contents.
+ */
+ public void close() {
+ document.delete();
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadHelper.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadHelper.java
new file mode 100644
index 0000000..aaa1436
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadHelper.java
@@ -0,0 +1,128 @@
+package com.amazon.spapi.documents;
+
+import com.amazon.spapi.documents.exception.HttpResponseException;
+import com.amazon.spapi.documents.impl.OkHttpTransferClient;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Helper for downloading encrypted documents.
+ */
+public class DownloadHelper {
+ private final HttpTransferClient httpTransferClient;
+ private final String tmpFilePrefix;
+ private final String tmpFileSuffix;
+ private final File tmpFileDirectory;
+
+ private DownloadHelper(HttpTransferClient httpTransferClient, String tmpFilePrefix, String tmpFileSuffix,
+ File tmpFileDirectory) {
+ this.httpTransferClient = httpTransferClient;
+ this.tmpFilePrefix = tmpFilePrefix;
+ this.tmpFileSuffix = tmpFileSuffix;
+ this.tmpFileDirectory = tmpFileDirectory;
+ }
+
+ /**
+ * Download the specified document's encrypted contents to a temporary file on disk. It is the responsibility of the
+ * caller to call close
on the returned {@link AutoCloseable} {@link DownloadBundle}.
+ *
+ * Common reasons for receiving a 403 response include:
+ * The signed URL has expired
+ *
+ * @param spec The specification for the download
+ * @return The closeable {@link DownloadBundle}
+ * @throws HttpResponseException On failure HTTP response
+ * @throws IOException IO Exception
+ */
+ public DownloadBundle download(DownloadSpecification spec) throws HttpResponseException, IOException {
+
+ File tmpFile = File.createTempFile(tmpFilePrefix, tmpFileSuffix, tmpFileDirectory);
+
+ try {
+ tmpFile.deleteOnExit();
+
+ String contentType = httpTransferClient.download(spec.getUrl(), tmpFile);
+
+ return new DownloadBundle(
+ spec.getCompressionAlgorithm(), contentType, spec.getCryptoStreamFactory(), tmpFile);
+ } catch (Exception e) {
+ tmpFile.delete();
+ throw e;
+ }
+ }
+
+ /**
+ * Use this to create an instance of a {@link DownloadHelper}.
+ */
+ public static class Builder {
+ private HttpTransferClient httpTransferClient = null;
+ private String tmpFilePrefix = "SPAPI";
+ private String tmpFileSuffix = null;
+ private File tmpFileDirectory = null;
+
+ /**
+ * The HTTP transfer client.
+ *
+ * @param httpTransferClient The HTTP transfer client
+ * @return this
+ */
+ public Builder withHttpTransferClient(HttpTransferClient httpTransferClient) {
+ this.httpTransferClient = httpTransferClient;
+ return this;
+ }
+
+ /**
+ * The tmp file prefix. If not specified, defaults to "SPAPI"
.
+ *
+ * @param tmpFilePrefix The prefix string to be used in generating the tmp file's name; must be at least three
+ * characters long
+ * @return this
+ */
+ public Builder withTmpFilePrefix(String tmpFilePrefix) {
+ if (tmpFilePrefix.length() < 3) {
+ throw new IllegalArgumentException("Prefix string too short");
+ }
+
+ this.tmpFilePrefix = tmpFilePrefix;
+ return this;
+ }
+
+ /**
+ * The tmp file suffix. If not specified, defaults to null.
+ *
+ * @param tmpFileSuffix The suffix string to be used in generating the file's name; may be null
, in
+ * which case the suffix ".tmp"
will be used
+ * @return this
+ */
+ public Builder withTmpFileSuffix(String tmpFileSuffix) {
+ this.tmpFileSuffix = tmpFileSuffix;
+ return this;
+ }
+
+ /**
+ * The tmp file directory. If not specified, defaults to null.
+ *
+ * @param tmpFileDirectory The directory in which the file is to be created, or null
if the default
+ * temporary-file directory is to be used
+ * @return this
+ */
+ public Builder withTmpFileDirectory(File tmpFileDirectory) {
+ this.tmpFileDirectory = tmpFileDirectory;
+ return this;
+ }
+
+ /**
+ * Create the helper.
+ *
+ * @return The helper
+ */
+ public DownloadHelper build() {
+ if (httpTransferClient == null) {
+ httpTransferClient = new OkHttpTransferClient.Builder().build();
+ }
+
+ return new DownloadHelper(httpTransferClient, tmpFilePrefix, tmpFileSuffix, tmpFileDirectory);
+ }
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadSpecification.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadSpecification.java
new file mode 100644
index 0000000..6b03869
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/DownloadSpecification.java
@@ -0,0 +1,76 @@
+package com.amazon.spapi.documents;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Specification for {@link DownloadHelper#download(DownloadSpecification)}.
+ */
+public class DownloadSpecification {
+ private final CompressionAlgorithm compressionAlgorithm;
+ private final CryptoStreamFactory cryptoStreamFactory;
+ private final String url;
+
+ private DownloadSpecification(CompressionAlgorithm compressionAlgorithm, CryptoStreamFactory cryptoStreamFactory,
+ String url) {
+ this.compressionAlgorithm = compressionAlgorithm;
+ this.cryptoStreamFactory = cryptoStreamFactory;
+ this.url = url;
+ }
+
+ CompressionAlgorithm getCompressionAlgorithm() {
+ return compressionAlgorithm;
+ }
+
+ CryptoStreamFactory getCryptoStreamFactory() {
+ return cryptoStreamFactory;
+ }
+
+ String getUrl() {
+ return url;
+ }
+
+ /**
+ * Use this to create an instance of a {@link DownloadSpecification}.
+ */
+ public static class Builder {
+ private final CryptoStreamFactory cryptoStreamFactory;
+ private final String url;
+
+ private CompressionAlgorithm compressionAlgorithm = null;
+
+ /**
+ * Create the builder.
+ *
+ * @param cryptoStreamFactory The crypto stream factory
+ * @param url The url to download the encrypted document from
+ */
+ public Builder(CryptoStreamFactory cryptoStreamFactory, String url) {
+ Preconditions.checkArgument(cryptoStreamFactory != null, "cryptoStreamFactory is required");
+ Preconditions.checkArgument(url != null, "url is required");
+
+ this.cryptoStreamFactory = cryptoStreamFactory;
+ this.url = url;
+ }
+
+ /**
+ * The compression algorithm.
+ *
+ * @param compressionAlgorithm The compression algorithm
+ * @return this
+ */
+ public Builder withCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) {
+ this.compressionAlgorithm = compressionAlgorithm;
+ return this;
+ }
+
+ /**
+ * Create the specification.
+ *
+ * @return The specification
+ */
+ public DownloadSpecification build() {
+ return new DownloadSpecification(compressionAlgorithm, cryptoStreamFactory, url);
+ }
+
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/HttpTransferClient.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/HttpTransferClient.java
new file mode 100644
index 0000000..ec2dc80
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/HttpTransferClient.java
@@ -0,0 +1,34 @@
+package com.amazon.spapi.documents;
+
+import com.amazon.spapi.documents.exception.HttpResponseException;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * HTTP transfer client. Implementations of this interface must be thread-safe and reusable for multiple requests.
+ */
+public interface HttpTransferClient {
+ /**
+ * Perform an HTTP GET on the specified url
, storing the response body to destination
.
+ *
+ * @param url The url to perform an HTTP GET on
+ * @param destination The file to write the HTTP GET response body to
+ * @return The Content-Type header value extracted from the response to the HTTP GET
+ * @throws HttpResponseException On failure HTTP response
+ * @throws IOException IO exception
+ */
+ String download(String url, File destination) throws HttpResponseException, IOException;
+
+ /**
+ * Perform an HTTP PUT on the specified url
, uploading the contents of source
to the body
+ * of the request.
+ *
+ * @param url The url to perform an HTTP PUT on
+ * @param contentType The Content-Type header to be used for the HTTP PUT request
+ * @param source The file to read the HTTP PUT body from
+ * @throws HttpResponseException On failure HTTP response
+ * @throws IOException IO exception
+ */
+ void upload(String url, String contentType, File source) throws HttpResponseException, IOException;
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/UploadHelper.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/UploadHelper.java
new file mode 100644
index 0000000..553f122
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/UploadHelper.java
@@ -0,0 +1,131 @@
+package com.amazon.spapi.documents;
+
+import com.amazon.spapi.documents.exception.CryptoException;
+import com.amazon.spapi.documents.exception.HttpResponseException;
+import com.amazon.spapi.documents.impl.OkHttpTransferClient;
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Helper class for encrypting and uploading documents.
+ */
+public class UploadHelper {
+ private final HttpTransferClient httpTransferClient;
+ private final String tmpFilePrefix;
+ private final String tmpFileSuffix;
+ private final File tmpFileDirectory;
+
+ private UploadHelper(HttpTransferClient httpTransferClient, String tmpFilePrefix, String tmpFileSuffix,
+ File tmpFileDirectory) {
+ this.httpTransferClient = httpTransferClient;
+ this.tmpFilePrefix = tmpFilePrefix;
+ this.tmpFileSuffix = tmpFileSuffix;
+ this.tmpFileDirectory = tmpFileDirectory;
+ }
+
+ /**
+ * Perform the specified upload. This method will buffer the encrypted contents of the document in a temporary file
+ * before uploading to the specified url.
+ *
+ * Common reasons for receiving a 403 response include:
+ * The signed URL has expired
+ * {@link UploadSpecification#getContentType()} does not match the content type the URL was signed with
+ *
+ * @param spec The specification for the upload
+ * @throws CryptoException Crypto exception
+ * @throws HttpResponseException On failure HTTP response
+ * @throws IOException IO exception
+ */
+ public void upload(UploadSpecification spec) throws CryptoException, HttpResponseException, IOException {
+ File tmpFile = File.createTempFile(tmpFilePrefix, tmpFileSuffix, tmpFileDirectory);
+
+ try {
+ tmpFile.deleteOnExit();
+
+ try (InputStream inputStream = spec.getCryptoStreamFactory().newEncryptStream(spec.getSource())) {
+ FileUtils.copyInputStreamToFile(inputStream, tmpFile);
+ }
+
+ httpTransferClient.upload(spec.getUrl(), spec.getContentType(), tmpFile);
+ } finally {
+ tmpFile.delete();
+ }
+ }
+
+ /**
+ * Use this to create an instance of an {@link UploadHelper}.
+ */
+ public static class Builder {
+ private HttpTransferClient httpTransferClient = null;
+ private String tmpFilePrefix = "SPAPI";
+ private String tmpFileSuffix = null;
+ private File tmpFileDirectory = null;
+
+ /**
+ * The HTTP transfer client.
+ *
+ * @param httpTransferClient The HTTP transfer client.
+ * @return this
+ */
+ public Builder withHttpTransferClient(HttpTransferClient httpTransferClient) {
+ this.httpTransferClient = httpTransferClient;
+ return this;
+ }
+
+ /**
+ * The tmp file prefix. If not specified, defaults to "SPAPI"
.
+ *
+ * @param tmpFilePrefix The prefix string to be used in generating the tmp file's name; must be at least three
+ * characters long
+ * @return this
+ */
+ public Builder withTmpFilePrefix(String tmpFilePrefix) {
+ if (tmpFilePrefix.length() < 3) {
+ throw new IllegalArgumentException("Prefix string too short");
+ }
+
+ this.tmpFilePrefix = tmpFilePrefix;
+ return this;
+ }
+
+ /**
+ * The tmp file suffix. If not specified, defaults to null.
+ *
+ * @param tmpFileSuffix The suffix string to be used in generating the file's name; may be null
, in
+ * which case the suffix ".tmp"
will be used
+ * @return this
+ */
+ public Builder withTmpFileSuffix(String tmpFileSuffix) {
+ this.tmpFileSuffix = tmpFileSuffix;
+ return this;
+ }
+
+ /**
+ * The tmp file directory. If not specified, defaults to null.
+ *
+ * @param tmpFileDirectory The directory in which the file is to be created, or null
if the default
+ * temporary-file directory is to be used
+ * @return this
+ */
+ public Builder withTmpFileDirectory(File tmpFileDirectory) {
+ this.tmpFileDirectory = tmpFileDirectory;
+ return this;
+ }
+
+ /**
+ * Create the helper.
+ *
+ * @return The helper.
+ */
+ public UploadHelper build() {
+ if (httpTransferClient == null) {
+ httpTransferClient = new OkHttpTransferClient.Builder().build();
+ }
+
+ return new UploadHelper(httpTransferClient, tmpFilePrefix, tmpFileSuffix, tmpFileDirectory);
+ }
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/UploadSpecification.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/UploadSpecification.java
new file mode 100644
index 0000000..eb93d1f
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/UploadSpecification.java
@@ -0,0 +1,79 @@
+package com.amazon.spapi.documents;
+
+import com.google.common.base.Preconditions;
+
+import java.io.InputStream;
+
+/**
+ * Specification for {@link UploadHelper#upload(UploadSpecification)}.
+ */
+public class UploadSpecification {
+ private final String contentType;
+ private final CryptoStreamFactory cryptoStreamFactory;
+ private final InputStream source;
+ private final String url;
+
+ private UploadSpecification(String contentType, CryptoStreamFactory cryptoStreamFactory, InputStream source,
+ String url) {
+ this.contentType = contentType;
+ this.cryptoStreamFactory = cryptoStreamFactory;
+ this.source = source;
+ this.url = url;
+ }
+
+ String getContentType() {
+ return contentType;
+ }
+
+ CryptoStreamFactory getCryptoStreamFactory() {
+ return cryptoStreamFactory;
+ }
+
+ InputStream getSource() {
+ return source;
+ }
+
+ String getUrl() {
+ return url;
+ }
+
+ /**
+ * Use this to create an instance of an {@link UploadSpecification}.
+ */
+ public static class Builder {
+ private final String contentType;
+ private final CryptoStreamFactory cryptoStreamFactory;
+ private final InputStream source;
+ private final String url;
+
+ /**
+ * Create the builder.
+ *
+ * @param contentType The content type of the document to upload
+ * @param cryptoStreamFactory The crypto stream factory
+ * @param source The source of the unencrypted data to upload
+ * @param url The url
+ */
+ public Builder(String contentType, CryptoStreamFactory cryptoStreamFactory, InputStream source, String url) {
+ Preconditions.checkArgument(contentType != null, "contentType is required");
+ Preconditions.checkArgument(cryptoStreamFactory != null, "cryptoStreamFactory is required");
+ Preconditions.checkArgument(source != null, "source is required");
+ Preconditions.checkArgument(url != null, "url is required");
+
+ this.contentType = contentType;
+ this.cryptoStreamFactory = cryptoStreamFactory;
+ this.source = source;
+ this.url = url;
+ }
+
+ /**
+ * Create the specification.
+ *
+ * @return The specification
+ */
+ public UploadSpecification build() {
+ return new UploadSpecification(contentType, cryptoStreamFactory, source, url);
+ }
+
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/CryptoException.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/CryptoException.java
new file mode 100644
index 0000000..1e36f72
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/CryptoException.java
@@ -0,0 +1,13 @@
+package com.amazon.spapi.documents.exception;
+
+/**
+ * Crypto exception.
+ */
+public class CryptoException extends Exception {
+ /**
+ * {@inheritDoc}
+ */
+ public CryptoException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/HttpResponseException.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/HttpResponseException.java
new file mode 100644
index 0000000..c749c2c
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/HttpResponseException.java
@@ -0,0 +1,63 @@
+package com.amazon.spapi.documents.exception;
+
+/**
+ * The details of an HTTP response that indicates failure.
+ */
+public class HttpResponseException extends Exception {
+ private final String body;
+ private final int code;
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param message The {@link Exception} message
+ * @param body The body
+ * @param code The HTTP status code
+ */
+ public HttpResponseException(String message, String body, int code) {
+ super(message);
+ this.body = body;
+ this.code = code;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param message The {@link Exception} message
+ * @param cause The {@link Exception} cause
+ * @param body The body
+ * @param code The HTTP status code
+ */
+ public HttpResponseException(String message, Throwable cause, String body, int code) {
+ super(message, cause);
+ this.body = body;
+ this.code = code;
+ }
+
+ /**
+ * The body. To ensure that a remote server cannot overwhelm heap memory, the body may have been truncated.
+ *
+ * @return The body
+ */
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * The HTTP status code
+ *
+ * @return The HTTP status code
+ */
+ public int getCode() {
+ return code;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " {code="
+ + getCode()
+ + ", body="
+ + getBody()
+ + '}';
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/MissingCharsetException.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/MissingCharsetException.java
new file mode 100644
index 0000000..693431a
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/exception/MissingCharsetException.java
@@ -0,0 +1,13 @@
+package com.amazon.spapi.documents.exception;
+
+/**
+ * Missing charset exception.
+ */
+public class MissingCharsetException extends Exception {
+ /**
+ * {@inheritDoc}
+ */
+ public MissingCharsetException(String message) {
+ super(message);
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/impl/AESCryptoStreamFactory.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/impl/AESCryptoStreamFactory.java
new file mode 100644
index 0000000..d6b0fc5
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/impl/AESCryptoStreamFactory.java
@@ -0,0 +1,96 @@
+package com.amazon.spapi.documents.impl;
+
+import com.amazon.spapi.documents.CryptoStreamFactory;
+import com.amazon.spapi.documents.exception.CryptoException;
+import com.google.common.base.Preconditions;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+/**
+ * A crypto stream factory implementing AES encryption.
+ */
+public class AESCryptoStreamFactory implements CryptoStreamFactory {
+ private static final String ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding";
+
+ private final Key key;
+ private final byte[] initializationVector;
+
+ private AESCryptoStreamFactory(Key key, byte[] initializationVector) {
+ this.key = key;
+ this.initializationVector = initializationVector;
+ }
+
+ private Cipher createInitializedCipher(int mode) throws CryptoException {
+ try {
+ Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
+ cipher.init(mode, key, new IvParameterSpec(initializationVector));
+ return cipher;
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException | NoSuchAlgorithmException |
+ NoSuchPaddingException e) {
+ throw new CryptoException(e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStream newDecryptStream(InputStream source) throws CryptoException {
+ return new CipherInputStream(source, createInitializedCipher(Cipher.DECRYPT_MODE));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStream newEncryptStream(InputStream source) throws CryptoException {
+ return new CipherInputStream(source, createInitializedCipher(Cipher.ENCRYPT_MODE));
+ }
+
+ /**
+ * Use this to create an instance of an {@link AESCryptoStreamFactory}.
+ */
+ public static class Builder {
+ private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
+ private static final String REQUIRED_KEY_ALGORITHM = "AES";
+
+ private final String key;
+ private final String initializationVector;
+
+ /**
+ * Create the builder.
+ *
+ * @param key The key
+ * @param initializationVector The initialization vector
+ */
+ public Builder(String key, String initializationVector) {
+ Preconditions.checkArgument(key != null, "key is required");
+ Preconditions.checkArgument(initializationVector != null, "initializationVector is required");
+
+ this.key = key;
+ this.initializationVector = initializationVector;
+ }
+
+ /**
+ * Create the crypto stream factory.
+ *
+ * @return the crypto stream factory
+ */
+ public AESCryptoStreamFactory build() {
+ Key convertedKey = new SecretKeySpec(BASE64_DECODER.decode(key), REQUIRED_KEY_ALGORITHM);
+ byte[] convertedInitializationVector = BASE64_DECODER.decode(initializationVector);
+
+ return new AESCryptoStreamFactory(convertedKey, convertedInitializationVector);
+ }
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/impl/OkHttpTransferClient.java b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/impl/OkHttpTransferClient.java
new file mode 100644
index 0000000..6d682bd
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/main/java/com/amazon/spapi/documents/impl/OkHttpTransferClient.java
@@ -0,0 +1,136 @@
+package com.amazon.spapi.documents.impl;
+
+import com.amazon.spapi.documents.HttpTransferClient;
+import com.amazon.spapi.documents.exception.HttpResponseException;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+
+/**
+ * HTTP transfer client utilizing OkHttp.
+ */
+public class OkHttpTransferClient implements HttpTransferClient {
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+ private final OkHttpClient client;
+ private final int maxErrorBodyLen;
+
+ private OkHttpTransferClient(OkHttpClient client, int maxErrorBodyLen) {
+ this.client = client;
+ this.maxErrorBodyLen = maxErrorBodyLen;
+ }
+
+ private HttpResponseException createResponseException(Response response) {
+ String body = "";
+ if (maxErrorBodyLen > 0) {
+ try (Reader bodyReader = response.body().charStream()) {
+ char[] buf = new char[maxErrorBodyLen];
+ int charsRead = IOUtils.read(bodyReader, buf);
+ if (charsRead > 0) {
+ body = new String(buf, 0, charsRead);
+ }
+ } catch (Exception e) {
+ // Ignore any failures reading the body so that the original failure is not lost
+ }
+ }
+
+ return new HttpResponseException(response.message(), body, response.code());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String download(String url, File destination) throws HttpResponseException, IOException {
+ Request request = new Request.Builder()
+ .url(url)
+ .get()
+ .build();
+
+ Response response = client.newCall(request).execute();
+ try {
+ if (!response.isSuccessful()) {
+ throw createResponseException(response);
+ }
+
+ FileUtils.copyInputStreamToFile(response.body().byteStream(), destination);
+ } finally {
+ IOUtils.closeQuietly(response.body());
+ }
+
+ return response.header(CONTENT_TYPE_HEADER);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void upload(String url, String contentType, File source) throws HttpResponseException, IOException {
+ Request request = new Request.Builder()
+ .url(url)
+ .put(RequestBody.create(MediaType.parse(contentType), source))
+ .build();
+
+ Response response = client.newCall(request).execute();
+ try {
+ if (!response.isSuccessful()) {
+ throw createResponseException(response);
+ }
+ } finally {
+ IOUtils.closeQuietly(response.body());
+ }
+ }
+
+ /**
+ * Use this to create an instance of an {@link OkHttpTransferClient}.
+ */
+ public static class Builder {
+ private OkHttpClient client = null;
+ private int maxErrorBodyLen = 4096;
+
+ /**
+ * The {@link OkHttpClient}. If not specified, a new instance of {@link OkHttpClient} will be created using the
+ * no-arg constructor.
+ *
+ * @param client The client
+ * @return this
+ */
+ public Builder withClient(OkHttpClient client) {
+ this.client = client;
+ return this;
+ }
+
+ /**
+ * When an HTTP response indicates failure, the maximum number of characters for
+ * {@link HttpResponseException#getBody()}. Default 4096
.
+ *
+ * @param maxErrorBodyLen The maximum number of characters to extract from the response body on failure
+ * @return this
+ */
+ public Builder withMaxErrorBodyLen(int maxErrorBodyLen) {
+ this.maxErrorBodyLen = maxErrorBodyLen;
+ return this;
+ }
+
+ /**
+ * Create the client.
+ *
+ * @return The client
+ */
+ public OkHttpTransferClient build() {
+ if (client == null) {
+ client = new OkHttpClient();
+ }
+
+ return new OkHttpTransferClient(client, maxErrorBodyLen);
+ }
+ }
+}
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/CompressionAlgorithmTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/CompressionAlgorithmTest.java
new file mode 100644
index 0000000..511581b
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/CompressionAlgorithmTest.java
@@ -0,0 +1,46 @@
+package com.amazon.spapi.documents;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CompressionAlgorithmTest {
+ public enum MyEnum {
+ GZIP, NOT_GZIP
+ }
+
+ @Test
+ public void testFromEquivalent() {
+ assertEquals(CompressionAlgorithm.GZIP, CompressionAlgorithm.fromEquivalent(MyEnum.GZIP));
+ }
+
+ @Test
+ public void testFromEquivalentNull() {
+ MyEnum myEnum = null;
+ assertNull(CompressionAlgorithm.fromEquivalent(myEnum));
+ }
+
+ @Test
+ public void testNotEquivalent() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> CompressionAlgorithm.fromEquivalent(MyEnum.NOT_GZIP));
+ }
+
+ @Test
+ public void testFromString() {
+ assertEquals(CompressionAlgorithm.GZIP, CompressionAlgorithm.fromEquivalent("GZIP"));
+ }
+
+ @Test
+ public void testFromStringNull() {
+ String val = null;
+ assertNull(CompressionAlgorithm.fromEquivalent(val));
+ }
+
+ @Test
+ public void testFromStringUnsupportedValue() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> CompressionAlgorithm.fromEquivalent("NOT_GZIP"));
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadBundleTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadBundleTest.java
new file mode 100644
index 0000000..793207e
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadBundleTest.java
@@ -0,0 +1,36 @@
+package com.amazon.spapi.documents;
+
+import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DownloadBundleTest {
+ private static String KEY = "sxx/wImF6BFndqSAz56O6vfiAh8iD9P297DHfFgujec=";
+ private static String VECTOR = "7S2tn363v0wfCfo1IX2Q1A==";
+
+ private DownloadBundle createBadFileBundle() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+
+ File file = File.createTempFile("foo", null, null);
+ file.delete();
+
+ return new DownloadBundle(null, contentType, cryptoStreamFactory, file);
+ }
+
+ @Test
+ public void testNewInputStreamBadFile() throws Exception {
+ DownloadBundle downloadBundle = createBadFileBundle();
+ assertThrows(FileNotFoundException.class, () -> downloadBundle.newInputStream());
+ }
+
+ @Test
+ public void testNewReaderBadFile() throws Exception {
+ DownloadBundle downloadBundle = createBadFileBundle();
+ assertThrows(FileNotFoundException.class, () -> downloadBundle.newBufferedReader());
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadHelperTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadHelperTest.java
new file mode 100644
index 0000000..b9d46d3
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadHelperTest.java
@@ -0,0 +1,403 @@
+package com.amazon.spapi.documents;
+
+import com.amazon.spapi.documents.exception.MissingCharsetException;
+import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.zip.GZIPOutputStream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DownloadHelperTest {
+ private static String KEY = "sxx/wImF6BFndqSAz56O6vfiAh8iD9P297DHfFgujec=";
+ private static String VECTOR = "7S2tn363v0wfCfo1IX2Q1A==";
+
+ private Answer getTransferAnswer(String contentType, CryptoStreamFactory cryptoStreamFactory, String fileContents,
+ boolean isGZipped) {
+ return new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ File file = invocation.getArgument(1);
+
+ byte[] dataToWrite = fileContents.getBytes(StandardCharsets.UTF_8);
+
+ if (isGZipped) {
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream(dataToWrite.length);
+ GZIPOutputStream zipStream = new GZIPOutputStream(byteStream);
+ zipStream.write(dataToWrite);
+ zipStream.close();
+ dataToWrite = byteStream.toByteArray();
+ }
+
+ // Write encrypted contents to file to mimic downloading an encrypted file
+ InputStream source = new ByteArrayInputStream(dataToWrite);
+ try (InputStream inputStream = cryptoStreamFactory.newEncryptStream(source)) {
+ FileUtils.copyInputStreamToFile(inputStream, file);
+ }
+
+ return contentType;
+ }
+ };
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ String url = "http://www.abc.com/123";
+ String expectedFileRegex = "SPAPI.*\\.tmp$";
+ String expectContents = "Hello World";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+ ArgumentCaptor fileArg = ArgumentCaptor.forClass(File.class);
+ Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
+ .when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ DownloadHelper helper = new DownloadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .build();
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
+
+ File file;
+ try (DownloadBundle bundle = helper.download(spec)) {
+ assertEquals(contentType, bundle.getContentType());
+
+ file = fileArg.getValue();
+ assertTrue(file.exists());
+
+ assertTrue(file.getName().matches(expectedFileRegex));
+
+ try (InputStream contents = bundle.newInputStream()) {
+ assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
+ StandardCharsets.UTF_8))));
+ }
+ }
+
+ assertFalse(file.exists());
+
+ Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
+ }
+
+ @Test
+ public void testSuccessWithCompression() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ String url = "http://www.abc.com/123";
+ String expectedFileRegex = "SPAPI.*\\.tmp$";
+ String expectContents = "Hello World";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+ ArgumentCaptor fileArg = ArgumentCaptor.forClass(File.class);
+ Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, true))
+ .when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ DownloadHelper helper = new DownloadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .build();
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url)
+ .withCompressionAlgorithm(CompressionAlgorithm.GZIP)
+ .build();
+
+ File file;
+ try (DownloadBundle bundle = helper.download(spec)) {
+ assertEquals(contentType, bundle.getContentType());
+
+ file = fileArg.getValue();
+ assertTrue(file.exists());
+
+ assertTrue(file.getName().matches(expectedFileRegex));
+
+ try (InputStream contents = bundle.newInputStream()) {
+ assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
+ StandardCharsets.UTF_8))));
+ }
+
+ try (BufferedReader contents = bundle.newBufferedReader()) {
+ assertTrue(IOUtils.contentEquals(contents, new StringReader(expectContents)));
+ }
+
+ assertTrue(file.exists());
+ }
+
+ assertFalse(file.exists());
+
+ Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
+ }
+
+ @Test
+ public void testSuccessWithCustomTempFileParams() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ String url = "http://www.abc.com/123";
+ String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
+ String expectContents = "Hello World";
+ String expectedFilePath = "spapitmp";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+ ArgumentCaptor fileArg = ArgumentCaptor.forClass(File.class);
+ Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
+ .when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
+ .toAbsolutePath().toString());
+ tmpDir.mkdirs();
+ tmpDir.deleteOnExit();
+
+ DownloadHelper helper = new DownloadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .withTmpFilePrefix("NOTSPAPI")
+ .withTmpFileSuffix(".NOTtmp")
+ .withTmpFileDirectory(tmpDir)
+ .build();
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
+
+ File file;
+ try (DownloadBundle bundle = helper.download(spec)) {
+ assertEquals(contentType, bundle.getContentType());
+
+ file = fileArg.getValue();
+ assertTrue(file.exists());
+ assertEquals(expectedFilePath, file.getParentFile().getName());
+
+ assertTrue(file.getName().matches(expectedFileRegex));
+
+ try (BufferedReader contents = bundle.newBufferedReader()) {
+ assertTrue(IOUtils.contentEquals(contents, new StringReader(expectContents)));
+ }
+
+ try (InputStream contents = bundle.newInputStream()) {
+ assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
+ StandardCharsets.UTF_8))));
+ }
+
+ assertTrue(file.exists());
+ }
+
+ assertFalse(file.exists());
+
+ Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
+ }
+
+ @Test
+ public void testReaderBadCharset() throws Exception {
+ String contentType = "text/xml; charset=NOTSUPPORTED";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ String url = "http://www.abc.com/123";
+ String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
+ String expectContents = "Hello World";
+ String expectedFilePath = "spapitmp";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+ ArgumentCaptor fileArg = ArgumentCaptor.forClass(File.class);
+ Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
+ .when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
+ .toAbsolutePath().toString());
+ tmpDir.mkdirs();
+ tmpDir.deleteOnExit();
+
+ DownloadHelper helper = new DownloadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .withTmpFilePrefix("NOTSPAPI")
+ .withTmpFileSuffix(".NOTtmp")
+ .withTmpFileDirectory(tmpDir)
+ .build();
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
+
+ File file;
+ try (DownloadBundle bundle = helper.download(spec)) {
+ assertEquals(contentType, bundle.getContentType());
+
+ file = fileArg.getValue();
+ assertTrue(file.exists());
+ assertEquals(expectedFilePath, file.getParentFile().getName());
+
+ assertTrue(file.getName().matches(expectedFileRegex));
+
+ assertThrows(UnsupportedCharsetException.class, () -> bundle.newBufferedReader());
+
+ try (InputStream contents = bundle.newInputStream()) {
+ assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
+ StandardCharsets.UTF_8))));
+ }
+
+ assertTrue(file.exists());
+ }
+
+ assertFalse(file.exists());
+
+ Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
+ }
+
+ @Test
+ public void testMissingCharsetNoDefault() throws Exception {
+ String contentType = "text/xml";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ String url = "http://www.abc.com/123";
+ String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
+ String expectContents = "Hello World";
+ String expectedFilePath = "spapitmp";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+ ArgumentCaptor fileArg = ArgumentCaptor.forClass(File.class);
+ Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
+ .when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
+ .toAbsolutePath().toString());
+ tmpDir.mkdirs();
+ tmpDir.deleteOnExit();
+
+ DownloadHelper helper = new DownloadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .withTmpFilePrefix("NOTSPAPI")
+ .withTmpFileSuffix(".NOTtmp")
+ .withTmpFileDirectory(tmpDir)
+ .build();
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
+
+ File file;
+ try (DownloadBundle bundle = helper.download(spec)) {
+ assertEquals(contentType, bundle.getContentType());
+
+ file = fileArg.getValue();
+ assertTrue(file.exists());
+ assertEquals(expectedFilePath, file.getParentFile().getName());
+
+ assertTrue(file.getName().matches(expectedFileRegex));
+
+ assertThrows(MissingCharsetException.class, () -> bundle.newBufferedReader());
+
+ try (InputStream contents = bundle.newInputStream()) {
+ assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
+ StandardCharsets.UTF_8))));
+ }
+
+ assertTrue(file.exists());
+ }
+
+ assertFalse(file.exists());
+
+ Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
+ }
+
+ @Test
+ public void testSuccessMissingBadCharsetDefaultProvided() throws Exception {
+ String contentType = "text/xml";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ String url = "http://www.abc.com/123";
+ String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
+ String expectContents = "Hello World";
+ String expectedFilePath = "spapitmp";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+ ArgumentCaptor fileArg = ArgumentCaptor.forClass(File.class);
+ Mockito.doReturn(contentType).when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ Mockito.doAnswer(getTransferAnswer(contentType, cryptoStreamFactory, expectContents, false))
+ .when(httpTransferClient).download(Mockito.eq(url), fileArg.capture());
+
+ File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
+ .toAbsolutePath().toString());
+ tmpDir.mkdirs();
+ tmpDir.deleteOnExit();
+
+ DownloadHelper helper = new DownloadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .withTmpFilePrefix("NOTSPAPI")
+ .withTmpFileSuffix(".NOTtmp")
+ .withTmpFileDirectory(tmpDir)
+ .build();
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
+
+ File file;
+ try (DownloadBundle bundle = helper.download(spec)) {
+ assertEquals(contentType, bundle.getContentType());
+
+ file = fileArg.getValue();
+ assertTrue(file.exists());
+ assertEquals(expectedFilePath, file.getParentFile().getName());
+
+ assertTrue(file.getName().matches(expectedFileRegex));
+
+ try (InputStream contents = bundle.newInputStream()) {
+ assertTrue(IOUtils.contentEquals(contents, new ByteArrayInputStream(expectContents.getBytes(
+ StandardCharsets.UTF_8))));
+ }
+
+ try (BufferedReader contents = bundle.newBufferedReader(StandardCharsets.UTF_8)) {
+ assertTrue(IOUtils.contentEquals(contents, new StringReader(expectContents)));
+ }
+
+ assertTrue(file.exists());
+ }
+
+ assertFalse(file.exists());
+
+ Mockito.verify(httpTransferClient).download(Mockito.eq(url), Mockito.any());
+ }
+
+ @Test
+ public void testBadUrlDownload() throws Exception {
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ String url = "sdfkajsdfiefi";
+
+ Path path = Files.createTempDirectory("SPAPI");
+
+ DownloadHelper helper = new DownloadHelper.Builder()
+ .withTmpFileDirectory(path.toFile())
+ .build();
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url)
+ .build();
+
+ try {
+ assertThrows(IllegalArgumentException.class, () -> helper.download(spec));
+ } finally {
+ // This will throw if the directory is not empty, indicating that the temp file was not removed
+ Files.delete(path);
+ }
+ }
+
+ @Test
+ public void testInvalidTmpFilePrefix() {
+ DownloadHelper.Builder builder = new DownloadHelper.Builder();
+
+ assertThrows(IllegalArgumentException.class, () -> builder
+ .withTmpFilePrefix("A"));
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadSpecificationTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadSpecificationTest.java
new file mode 100644
index 0000000..b9cac73
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/DownloadSpecificationTest.java
@@ -0,0 +1,53 @@
+package com.amazon.spapi.documents;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DownloadSpecificationTest {
+ @Test
+ public void testBuilderConstructorMissingCryptoStreamFactory() throws Exception {
+ CryptoStreamFactory cryptoStreamFactory = null;
+ String url = "https://www.amazon.com";
+
+ assertThrows(IllegalArgumentException.class, () ->
+ new DownloadSpecification.Builder(cryptoStreamFactory, url));
+ }
+
+ @Test
+ public void testBuilderConstructorMissingUrl() throws Exception {
+ CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
+ String url = null;
+
+ assertThrows(IllegalArgumentException.class, () ->
+ new DownloadSpecification.Builder(cryptoStreamFactory, url));
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
+ String url = "http://abc.com/123";
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url).build();
+
+ assertNull(spec.getCompressionAlgorithm());
+ assertSame(cryptoStreamFactory, spec.getCryptoStreamFactory());
+ assertEquals(url, spec.getUrl());
+ }
+
+ @Test
+ public void testCompressionAlgorithm() throws Exception {
+ CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.GZIP;
+ CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
+ String url = "http://abc.com/123";
+
+ DownloadSpecification spec = new DownloadSpecification.Builder(cryptoStreamFactory, url)
+ .withCompressionAlgorithm(compressionAlgorithm)
+ .build();
+
+ assertEquals(compressionAlgorithm, spec.getCompressionAlgorithm());
+ assertSame(cryptoStreamFactory, spec.getCryptoStreamFactory());
+ assertEquals(url, spec.getUrl());
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/UploadHelperTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/UploadHelperTest.java
new file mode 100644
index 0000000..ae925b1
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/UploadHelperTest.java
@@ -0,0 +1,171 @@
+package com.amazon.spapi.documents;
+
+import com.amazon.spapi.documents.impl.AESCryptoStreamFactory;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class UploadHelperTest {
+ private static String KEY = "sxx/wImF6BFndqSAz56O6vfiAh8iD9P297DHfFgujec=";
+ private static String VECTOR = "7S2tn363v0wfCfo1IX2Q1A==";
+
+ class FileCapture {
+ volatile boolean existed = false;
+ volatile File file = null;
+ }
+
+ private Answer getAnswer(FileCapture fileCapture, Exception e) {
+ return new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ File tmpFile = (File)invocation.getArgument(2);
+ fileCapture.existed = tmpFile.exists();
+ fileCapture.file = tmpFile;
+
+ if (e != null) {
+ throw e;
+ }
+
+ return null;
+ }
+ };
+ }
+
+ private Answer getAnswer(FileCapture fileCapture) {
+ return getAnswer(fileCapture, null);
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ InputStream source = new ByteArrayInputStream(new byte[0]);
+ String url = "http://www.abc.com/123";
+ String expectedFileRegex = "SPAPI.*\\.tmp$";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+
+ FileCapture fileCapture = new FileCapture();
+ Mockito.doAnswer(getAnswer(fileCapture)).when(httpTransferClient)
+ .upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
+
+ UploadHelper helper = new UploadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .build();
+
+ UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
+ .build();
+ helper.upload(spec);
+
+ assertTrue(fileCapture.file.getName().matches(expectedFileRegex));
+ assertTrue(fileCapture.existed);
+ assertFalse(fileCapture.file.exists());
+
+ Mockito.verify(httpTransferClient).upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
+ }
+
+ @Test
+ public void testSuccessWithCustomTmpFileParams() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ InputStream source = new ByteArrayInputStream(new byte[0]);
+ String url = "http://www.abc.com/123";
+ String expectedFileRegex = "NOTSPAPI.*\\.NOTtmp$";
+ String expectedFilePath = "spapitmp";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+
+ FileCapture fileCapture = new FileCapture();
+ Mockito.doAnswer(getAnswer(fileCapture)).when(httpTransferClient)
+ .upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
+
+ File tmpDir = new File(Paths.get(System.getProperty("java.io.tmpdir"), expectedFilePath)
+ .toAbsolutePath().toString());
+ tmpDir.mkdirs();
+
+ UploadHelper helper = new UploadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .withTmpFilePrefix("NOTSPAPI")
+ .withTmpFileSuffix(".NOTtmp")
+ .withTmpFileDirectory(tmpDir)
+ .build();
+
+ UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
+ .build();
+ helper.upload(spec);
+
+ assertTrue(fileCapture.file.getName().matches(expectedFileRegex));
+ assertEquals(expectedFilePath, fileCapture.file.getParentFile().getName());
+ assertTrue(fileCapture.existed);
+ assertFalse(fileCapture.file.exists());
+
+ Mockito.verify(httpTransferClient).upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
+ }
+
+ @Test
+ public void testFileDeletedOnFailure() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ InputStream source = new ByteArrayInputStream(new byte[0]);
+ String url = "http://www.abc.com/123";
+
+ HttpTransferClient httpTransferClient = Mockito.mock(HttpTransferClient.class);
+
+ FileCapture fileCapture = new FileCapture();
+ Mockito.doAnswer(getAnswer(fileCapture, new IllegalArgumentException())).when(httpTransferClient)
+ .upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
+
+ UploadHelper helper = new UploadHelper.Builder()
+ .withHttpTransferClient(httpTransferClient)
+ .build();
+
+ UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
+ .build();
+ assertThrows(IllegalArgumentException.class, () -> helper.upload(spec));
+
+ assertTrue(fileCapture.existed);
+ assertFalse(fileCapture.file.exists());
+
+ Mockito.verify(httpTransferClient).upload(Mockito.eq(url), Mockito.eq(contentType), Mockito.any());
+ }
+
+ @Test
+ public void testBadUrlFailedUpload() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+ InputStream source = new ByteArrayInputStream(new byte[0]);
+ String url = "sdfkajsdfiefi";
+
+ Path path = Files.createTempDirectory("SPAPI");
+ try {
+ UploadHelper helper = new UploadHelper.Builder()
+ .withTmpFileDirectory(path.toFile())
+ .build();
+
+ UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
+ .build();
+ assertThrows(IllegalArgumentException.class, () -> helper.upload(spec));
+ } finally {
+ // This will throw if the directory is not empty, indicating that the temp file was not removed
+ Files.delete(path);
+ }
+ }
+
+ @Test
+ public void testInvalidTmpFilePrefix() {
+ UploadHelper.Builder builder = new UploadHelper.Builder();
+
+ assertThrows(IllegalArgumentException.class, () -> builder
+ .withTmpFilePrefix("A"));
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/UploadSpecificationTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/UploadSpecificationTest.java
new file mode 100644
index 0000000..ff16734
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/UploadSpecificationTest.java
@@ -0,0 +1,71 @@
+package com.amazon.spapi.documents;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class UploadSpecificationTest {
+ @Test
+ public void testBuilderConstructorMissingContentType() throws Exception {
+ String contentType = null;
+ CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
+ InputStream source = new ByteArrayInputStream(new byte[0]);
+ String url = "https://www.amazon.com";
+
+ assertThrows(IllegalArgumentException.class, () -> new UploadSpecification.Builder(
+ contentType, cryptoStreamFactory, source, url));
+ }
+
+ @Test
+ public void testBuilderConstructorMissingCryptoStreamFactory() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = null;
+ InputStream source = new ByteArrayInputStream(new byte[0]);
+ String url = "https://www.amazon.com";
+
+ assertThrows(IllegalArgumentException.class, () -> new UploadSpecification.Builder(
+ contentType, cryptoStreamFactory, source, url));
+ }
+
+ @Test
+ public void testBuilderConstructorMissingSource() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
+ InputStream source = null;
+ String url = "https://www.amazon.com";
+
+ assertThrows(IllegalArgumentException.class, () -> new UploadSpecification.Builder(
+ contentType, cryptoStreamFactory, source, url));
+ }
+
+ @Test
+ public void testBuilderConstructorMissingUrl() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
+ InputStream source = new ByteArrayInputStream(new byte[0]);
+ String url = null;
+
+ assertThrows(IllegalArgumentException.class, () -> new UploadSpecification.Builder(
+ contentType, cryptoStreamFactory, source, url));
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ CryptoStreamFactory cryptoStreamFactory = Mockito.mock(CryptoStreamFactory.class);
+ InputStream source = new ByteArrayInputStream(new byte[0]);
+ String url = "http://abc.com/123";
+
+ UploadSpecification spec = new UploadSpecification.Builder(contentType, cryptoStreamFactory, source, url)
+ .build();
+
+ assertEquals(contentType, spec.getContentType());
+ assertSame(cryptoStreamFactory, spec.getCryptoStreamFactory());
+ assertSame(source, spec.getSource());
+ assertEquals(url, spec.getUrl());
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/CryptoExceptionTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/CryptoExceptionTest.java
new file mode 100644
index 0000000..dc35992
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/CryptoExceptionTest.java
@@ -0,0 +1,15 @@
+package com.amazon.spapi.documents.exception;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CryptoExceptionTest {
+ @Test
+ public void testConstructor() {
+ Throwable throwable = new RuntimeException();
+ CryptoException exception = new CryptoException(throwable);
+
+ assertSame(throwable, exception.getCause());
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/HttpResponseExceptionTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/HttpResponseExceptionTest.java
new file mode 100644
index 0000000..2dbd666
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/HttpResponseExceptionTest.java
@@ -0,0 +1,54 @@
+package com.amazon.spapi.documents.exception;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class HttpResponseExceptionTest {
+ @Test
+ public void testConstructor() {
+ String message = "This is the message";
+ String body = "This is the body";
+ int code = 403;
+ String expectToString = "com.amazon.spapi.documents.exception.HttpResponseException: " +
+ "This is the message {code=403, body=This is the body}";
+
+ HttpResponseException exception = new HttpResponseException(message, body, code);
+
+ assertEquals(message, exception.getMessage());
+ assertEquals(body, exception.getBody());
+ assertEquals(code, exception.getCode());
+ assertEquals(expectToString, exception.toString());
+ }
+
+ @Test
+ public void testConstructorNullBody() {
+ String message = "This is the message";
+ String body = null;
+ int code = 403;
+ String expectToString = "com.amazon.spapi.documents.exception.HttpResponseException: " +
+ "This is the message {code=403, body=null}";
+
+ HttpResponseException exception = new HttpResponseException(message, body, code);
+
+ assertEquals(message, exception.getMessage());
+ assertEquals(body, exception.getBody());
+ assertEquals(code, exception.getCode());
+ assertEquals(expectToString, exception.toString());
+ }
+
+ @Test
+ public void testConstructorCause() {
+ String message = "This is the message";
+ Throwable cause = new RuntimeException();
+ String body = "This is the body";
+ int code = 403;
+
+ HttpResponseException exception = new HttpResponseException(message, cause, body, code);
+
+ assertEquals(message, exception.getMessage());
+ assertSame(cause, exception.getCause());
+ assertEquals(body, exception.getBody());
+ assertEquals(code, exception.getCode());
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/MissingCharsetExceptionTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/MissingCharsetExceptionTest.java
new file mode 100644
index 0000000..ccf3a53
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/exception/MissingCharsetExceptionTest.java
@@ -0,0 +1,15 @@
+package com.amazon.spapi.documents.exception;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class MissingCharsetExceptionTest {
+ @Test
+ public void testConstructor() {
+ String message = "This is the message";
+ MissingCharsetException exception = new MissingCharsetException(message);
+
+ assertEquals(message, exception.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/impl/AESCryptoStreamFactoryTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/impl/AESCryptoStreamFactoryTest.java
new file mode 100644
index 0000000..0601230
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/impl/AESCryptoStreamFactoryTest.java
@@ -0,0 +1,69 @@
+package com.amazon.spapi.documents.impl;
+
+import com.amazon.spapi.documents.exception.CryptoException;
+import com.google.common.io.ByteStreams;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AESCryptoStreamFactoryTest {
+ private static String KEY = "sxx/wImF6BFndqSAz56O6vfiAh8iD9P297DHfFgujec=";
+ private static String VECTOR = "7S2tn363v0wfCfo1IX2Q1A==";
+
+ @Test
+ public void testBuilderConstructorMissingInitializationVector() {
+ String key = "DEF";
+ String initializationVector = null;
+
+ assertThrows(IllegalArgumentException.class, () ->
+ new AESCryptoStreamFactory.Builder(key, initializationVector));
+ }
+
+ @Test
+ public void testBuilderConstructorMissingKey() {
+ String key = null;
+ String initializationVector = "ABC";
+
+ assertThrows(IllegalArgumentException.class, () ->
+ new AESCryptoStreamFactory.Builder(key, initializationVector));
+ }
+
+ @Test
+ public void testBadKey() throws Exception {
+ byte[] decodedKey = Base64.getDecoder().decode(KEY);
+ String encodedKey = Base64.getEncoder().encodeToString(
+ Arrays.copyOfRange(decodedKey, 2, decodedKey.length-1));
+
+ AESCryptoStreamFactory aesCryptoStreamFactory = new AESCryptoStreamFactory.Builder(encodedKey, VECTOR).build();
+ try (InputStream inputStream = new ByteArrayInputStream("Hello World!".getBytes(StandardCharsets.UTF_8))) {
+ assertThrows(CryptoException.class, () -> aesCryptoStreamFactory.newDecryptStream(inputStream));
+ }
+ }
+
+ @Test
+ public void testEncryptDecrypt() throws Exception {
+ String stringContent = "Hello World!";
+ byte[] byteContent = stringContent.getBytes(StandardCharsets.UTF_8);
+
+ AESCryptoStreamFactory aesCryptoStreamFactory = new AESCryptoStreamFactory.Builder(KEY, VECTOR).build();
+
+ try (InputStream encryptStream =
+ aesCryptoStreamFactory.newEncryptStream(new ByteArrayInputStream(byteContent))) {
+ byte[] encryptedContent = ByteStreams.toByteArray(encryptStream);
+
+ try (InputStream decryptStream =
+ aesCryptoStreamFactory.newDecryptStream(new ByteArrayInputStream(encryptedContent))) {
+ byte[] decryptedContent = ByteStreams.toByteArray(decryptStream);
+
+ assertArrayEquals(decryptedContent, byteContent);
+ assertFalse(Arrays.equals(encryptedContent, decryptedContent));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/impl/OkHttpTransferClientTest.java b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/impl/OkHttpTransferClientTest.java
new file mode 100644
index 0000000..f18ed82
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/java/com/amazon/spapi/documents/impl/OkHttpTransferClientTest.java
@@ -0,0 +1,331 @@
+package com.amazon.spapi.documents.impl;
+
+import com.amazon.spapi.documents.exception.HttpResponseException;
+import com.squareup.okhttp.Call;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
+import okio.Buffer;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class OkHttpTransferClientTest {
+
+ @Test
+ public void testDefaultBuilder() throws Exception {
+ OkHttpTransferClient helper = new OkHttpTransferClient.Builder().build();
+ OkHttpClient client = (OkHttpClient)ReflectionUtils.tryToReadFieldValue(OkHttpTransferClient.class,
+ "client", helper).get();
+ }
+
+ @Test
+ public void testDownloadSuccess() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ String url = "https://www.s3-amazon.com/123";
+ String content = "Hello world!";
+
+ OkHttpClient client = Mockito.mock(OkHttpClient.class);
+ Call call = Mockito.mock(Call.class);
+
+ ArgumentCaptor requestArg = ArgumentCaptor.forClass(Request.class);
+ Mockito.doReturn(call).when(client).newCall(requestArg.capture());
+
+ Response response = Mockito.mock(Response.class);
+ Mockito.doReturn(response).when(call).execute();
+
+ Mockito.doReturn(contentType).when(response).header("Content-Type");
+
+ Mockito.doReturn(true).when(response).isSuccessful();
+
+ ResponseBody responseBody = Mockito.mock(ResponseBody.class);
+ Mockito.doReturn(responseBody).when(response).body();
+
+ Mockito.doReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)))
+ .when(responseBody).byteStream();
+
+ OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
+ .withClient(client)
+ .build();
+
+ File tmpFile = File.createTempFile("foo", null);
+ try {
+ assertEquals(contentType, helper.download(url, tmpFile));
+ assertTrue(IOUtils.contentEquals(new FileInputStream(tmpFile),
+ new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))));
+ } finally {
+ tmpFile.delete();
+ }
+
+ assertEquals(url, requestArg.getValue().urlString());
+ assertEquals("GET", requestArg.getValue().method());
+ Mockito.verify(responseBody).close();
+ }
+
+ @Test
+ public void testDownloadIOException() throws Exception {
+ String url = "https://www.s3-amazon.com/123";
+
+ OkHttpClient client = Mockito.mock(OkHttpClient.class);
+ Call call = Mockito.mock(Call.class);
+
+ ArgumentCaptor requestArg = ArgumentCaptor.forClass(Request.class);
+ Mockito.doReturn(call).when(client).newCall(requestArg.capture());
+
+ Mockito.doThrow(new IOException()).when(call).execute();
+
+ OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
+ .withClient(client)
+ .build();
+
+ File tmpFile = File.createTempFile("foo", null);
+ try {
+ Assertions.assertThrows(IOException.class, () -> helper.download(url, tmpFile));
+ } finally {
+ tmpFile.delete();
+ }
+ }
+
+ private void failureResponseMocks(Response response, ResponseBody responseBody,
+ int httpStatusCode, String message, String bodyContent) throws Exception {
+ Mockito.doReturn(httpStatusCode).when(response).code();
+ Mockito.doReturn(message).when(response).message();
+
+ Mockito.doReturn(new StringReader(bodyContent)).when(responseBody)
+ .charStream();
+ }
+
+ @Test
+ public void testDownloadFailure() throws Exception {
+ String url = "https://www.s3-amazon.com/123";
+ String responseBodyText = "This is the response body";
+ int maxErrorBodyLen = 5;
+ int httpStatusCode = 403;
+ String message = "Forbidden";
+
+ OkHttpClient client = Mockito.mock(OkHttpClient.class);
+ Call call = Mockito.mock(Call.class);
+
+ ArgumentCaptor requestArg = ArgumentCaptor.forClass(Request.class);
+ Mockito.doReturn(call).when(client).newCall(requestArg.capture());
+
+ Response response = Mockito.mock(Response.class);
+ Mockito.doReturn(response).when(call).execute();
+
+ ResponseBody responseBody = Mockito.mock(ResponseBody.class);
+ Mockito.doReturn(responseBody).when(response).body();
+ Mockito.doNothing().when(responseBody).close();
+
+ failureResponseMocks(response, responseBody, httpStatusCode, message, responseBodyText);
+
+ Mockito.doReturn(false).when(response).isSuccessful();
+
+ OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
+ .withClient(client)
+ .withMaxErrorBodyLen(maxErrorBodyLen)
+ .build();
+
+ File tmpFile = File.createTempFile("foo", null);
+ try {
+ helper.download(url, tmpFile);
+ fail("Expected exception");
+ } catch(HttpResponseException e) {
+ assertEquals(httpStatusCode, e.getCode());
+ assertEquals(message, e.getMessage());
+ assertEquals("This ", e.getBody());
+ } finally {
+ tmpFile.delete();
+ }
+
+ Mockito.verify(responseBody).close();
+ }
+
+ @Test
+ public void testFailureInBodyExtraction() throws Exception {
+ String url = "https://www.s3-amazon.com/123";
+ int maxErrorBodyLen = 5;
+ int httpStatusCode = 403;
+ String message = "Forbidden";
+
+ OkHttpClient client = Mockito.mock(OkHttpClient.class);
+ Call call = Mockito.mock(Call.class);
+
+ ArgumentCaptor requestArg = ArgumentCaptor.forClass(Request.class);
+ Mockito.doReturn(call).when(client).newCall(requestArg.capture());
+
+ Response response = Mockito.mock(Response.class);
+ Mockito.doReturn(response).when(call).execute();
+
+ ResponseBody responseBody = Mockito.mock(ResponseBody.class);
+ Mockito.doReturn(responseBody).when(response).body();
+ Mockito.doNothing().when(responseBody).close();
+
+ Mockito.doReturn(httpStatusCode).when(response).code();
+ Mockito.doReturn(message).when(response).message();
+
+ Mockito.doThrow(new IOException()).when(responseBody).charStream();
+
+ Mockito.doReturn(false).when(response).isSuccessful();
+
+ OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
+ .withClient(client)
+ .withMaxErrorBodyLen(maxErrorBodyLen)
+ .build();
+
+ File tmpFile = File.createTempFile("foo", null);
+ try {
+ helper.download(url, tmpFile);
+ fail("Expected exception");
+ } catch(HttpResponseException e) {
+ assertEquals(httpStatusCode, e.getCode());
+ assertEquals(message, e.getMessage());
+ assertEquals("", e.getBody());
+ } finally {
+ tmpFile.delete();
+ }
+
+ Mockito.verify(responseBody).close();
+ }
+
+ @Test
+ public void testUploadSuccess() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ String url = "https://www.s3-amazon.com/123";
+ String content = "Hello world!";
+
+ OkHttpClient client = Mockito.mock(OkHttpClient.class);
+ Call call = Mockito.mock(Call.class);
+
+ ArgumentCaptor requestArg = ArgumentCaptor.forClass(Request.class);
+ Mockito.doReturn(call).when(client).newCall(requestArg.capture());
+
+ Response response = Mockito.mock(Response.class);
+ Mockito.doReturn(response).when(call).execute();
+
+ Mockito.doReturn(true).when(response).isSuccessful();
+
+ ResponseBody responseBody = Mockito.mock(ResponseBody.class);
+ Mockito.doReturn(responseBody).when(response).body();
+ Mockito.doNothing().when(responseBody).close();
+
+ OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
+ .withClient(client)
+ .build();
+
+ File tmpFile = File.createTempFile("foo", null);
+ try {
+ try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
+ fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+ helper.upload(url, contentType, tmpFile);
+
+ Buffer buffer = new Buffer();
+ requestArg.getValue().body().writeTo(buffer);
+ assertEquals(content, buffer.readString(StandardCharsets.UTF_8));
+
+ assertEquals(url, requestArg.getValue().urlString());
+ assertEquals("PUT", requestArg.getValue().method());
+ assertEquals(contentType, requestArg.getValue().body().contentType().toString());
+ } finally {
+ tmpFile.delete();
+ }
+
+ Mockito.verify(responseBody).close();
+ }
+
+ @Test
+ public void testUploadIOException() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ String url = "https://www.s3-amazon.com/123";
+ String content = "Hello world!";
+
+ OkHttpClient client = Mockito.mock(OkHttpClient.class);
+ Call call = Mockito.mock(Call.class);
+
+ ArgumentCaptor requestArg = ArgumentCaptor.forClass(Request.class);
+ Mockito.doReturn(call).when(client).newCall(requestArg.capture());
+
+ Mockito.doThrow(new IOException()).when(call).execute();
+
+ OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
+ .withClient(client)
+ .build();
+
+ File tmpFile = File.createTempFile("foo", null);
+ try {
+ try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
+ fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+ Assertions.assertThrows(IOException.class, () -> helper.upload(url, contentType, tmpFile));
+ } finally {
+ tmpFile.delete();
+ }
+ }
+
+ @Test
+ public void testUploadFailure() throws Exception {
+ String contentType = "text/xml; charset=UTF-8";
+ String url = "https://www.s3-amazon.com/123";
+ String content = "Hello world!";
+
+ String responseBodyText = "This is the response body";
+ int maxErrorBodyLen = 5;
+ int httpStatusCode = 403;
+ String message = "Forbidden";
+
+ OkHttpClient client = Mockito.mock(OkHttpClient.class);
+ Call call = Mockito.mock(Call.class);
+
+ ArgumentCaptor requestArg = ArgumentCaptor.forClass(Request.class);
+ Mockito.doReturn(call).when(client).newCall(requestArg.capture());
+
+ Response response = Mockito.mock(Response.class);
+ Mockito.doReturn(response).when(call).execute();
+
+ Mockito.doReturn(false).when(response).isSuccessful();
+
+ ResponseBody responseBody = Mockito.mock(ResponseBody.class);
+ Mockito.doReturn(responseBody).when(response).body();
+ Mockito.doNothing().when(responseBody).close();
+ failureResponseMocks(response, responseBody, httpStatusCode, message, responseBodyText);
+
+ OkHttpTransferClient helper = new OkHttpTransferClient.Builder()
+ .withClient(client)
+ .withMaxErrorBodyLen(maxErrorBodyLen)
+ .build();
+
+ File tmpFile = File.createTempFile("foo", null);
+ try {
+ try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
+ fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+ helper.upload(url, contentType, tmpFile);
+ fail("Expected exception");
+ } catch(HttpResponseException e) {
+ assertEquals(httpStatusCode, e.getCode());
+ assertEquals(message, e.getMessage());
+ assertEquals("This ", e.getBody());
+ } finally {
+ tmpFile.delete();
+ }
+
+ Mockito.verify(responseBody).close();
+ }
+}
\ No newline at end of file
diff --git a/clients/sellingpartner-api-documents-helper-java/src/test/resources/mockito-extensions/org.mockito.plugins.Mockmaker b/clients/sellingpartner-api-documents-helper-java/src/test/resources/mockito-extensions/org.mockito.plugins.Mockmaker
new file mode 100644
index 0000000..ca6ee9c
--- /dev/null
+++ b/clients/sellingpartner-api-documents-helper-java/src/test/resources/mockito-extensions/org.mockito.plugins.Mockmaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file