Updated Documents Helper
This commit is contained in:
parent
b1d990cdf7
commit
42744a1b38
|
@ -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
|
|
@ -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
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.amazon.sellingpartnerapi</groupId>
|
||||||
|
<artifactId>sellingpartner-api-documents-helper-java</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
|
@ -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'
|
||||||
|
|
||||||
|
}
|
BIN
clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
clients/sellingpartner-api-documents-helper-java/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -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
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.amazon.sellingpartnerapi</groupId>
|
||||||
|
<artifactId>sellingpartner-api-documents-helper-java</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>2.7.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp</groupId>
|
||||||
|
<artifactId>logging-interceptor</artifactId>
|
||||||
|
<version>2.7.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.gsonfire</groupId>
|
||||||
|
<artifactId>gson-fire</artifactId>
|
||||||
|
<version>1.8.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.directory.studio</groupId>
|
||||||
|
<artifactId>org.apache.commons.io</artifactId>
|
||||||
|
<version>2.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>28.2-jre</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.threeten</groupId>
|
||||||
|
<artifactId>threetenbp</artifactId>
|
||||||
|
<version>1.3.5</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.junit.platform/junit-platform-commons -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.platform</groupId>
|
||||||
|
<artifactId>junit-platform-commons</artifactId>
|
||||||
|
<version>1.7.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>5.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<version>5.3.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-migrationsupport</artifactId>
|
||||||
|
<version>5.5.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>3.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-inline</artifactId>
|
||||||
|
<version>3.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>2.22.2</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-failsafe-plugin</artifactId>
|
||||||
|
<version>2.22.2</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.7.0</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
</build>
|
||||||
|
<properties>
|
||||||
|
<java.version>1.8</java.version>
|
||||||
|
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||||
|
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||||
|
<maven-plugin-version>1.0.0</maven-plugin-version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
</project>
|
|
@ -0,0 +1 @@
|
||||||
|
rootProject.name = "sellingpartner-api-documents-helper-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 <T extends Enum> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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 <code>defaultCharset</code> is specified, this method will attempt to open the reader
|
||||||
|
* with <code>defaultCharset</code>. 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
|
||||||
|
* <code>defaultCharset</code> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <code>close</code> on the returned {@link AutoCloseable} {@link DownloadBundle}.
|
||||||
|
*
|
||||||
|
* Common reasons for receiving a 403 response include:
|
||||||
|
* <li> 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 <code>"SPAPI"</code>.
|
||||||
|
*
|
||||||
|
* @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 <code>null</code>, in
|
||||||
|
* which case the suffix <code>".tmp"</code> 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 <code>null</code> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <code>url</code>, storing the response body to <code>destination</code>.
|
||||||
|
*
|
||||||
|
* @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 <code>url</code>, uploading the contents of <code>source</code> 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;
|
||||||
|
}
|
|
@ -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:
|
||||||
|
* <li> The signed URL has expired
|
||||||
|
* <li> {@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 <code>"SPAPI"</code>.
|
||||||
|
*
|
||||||
|
* @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 <code>null</code>, in
|
||||||
|
* which case the suffix <code>".tmp"</code> 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 <code>null</code> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.amazon.spapi.documents.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crypto exception.
|
||||||
|
*/
|
||||||
|
public class CryptoException extends Exception {
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
public CryptoException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <code>4096</code>.
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<File> 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<File> 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<File> 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<File> 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<File> 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<File> 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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Request> 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<Request> 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<Request> 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<Request> 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<Request> 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<Request> 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<Request> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
mock-maker-inline
|
Loading…
Reference in New Issue