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