diff --git a/.gitignore b/.gitignore
index 3d4bc4d..24affe0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
+###################################################################################################
+## Project Internal ###############################################################################
+###################################################################################################
+
+config.properties
+
###################################################################################################
## Visual Studio Code #############################################################################
###################################################################################################
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a5b7a3..010c0fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
# Changelog
-## Use me!
\ No newline at end of file
+## 0.1.0
+
+### Added
+
+- (DataSource) → AmazonProductDataSource as first external data source (#39)
+- ProductDTO as data holder structure for data from external sources (~ #39)
+- Config for credential, base URLs etc. (~ #39)
+- FormattingUtil for formatting price strings (~ #39)
diff --git a/SampleResponses/AmazonProductAPIModel.json b/SampleResponses/AmazonProductAPIModel.json
new file mode 100644
index 0000000..0d0cd3a
--- /dev/null
+++ b/SampleResponses/AmazonProductAPIModel.json
@@ -0,0 +1,544 @@
+{
+ "/products/2020-08-26/products/{productId}/offers": {
+ "get": {
+ "produces": ["application/json"],
+ "parameters": [
+ {
+ "description": "The Amazon Standard Item Identifier (ASIN) for the product.",
+ "in": "path",
+ "name": "productId",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "description": "The region where the customer wants to purchase the product.",
+ "enum": ["DE", "FR", "UK", "IT", "ES", "US", "CA", "JP"],
+ "x-docgen-enum-table-extension": [
+ {
+ "value": "DE",
+ "description": "Germany"
+ },
+ {
+ "value": "FR",
+ "description": "France"
+ },
+ {
+ "value": "UK",
+ "description": "United Kingdom"
+ },
+ {
+ "value": "IT",
+ "description": "Italy"
+ },
+ {
+ "value": "ES",
+ "description": "Spain"
+ },
+ {
+ "value": "US",
+ "description": "United States of America"
+ },
+ {
+ "value": "CA",
+ "description": "Canada"
+ },
+ {
+ "value": "JP",
+ "description": "Japan"
+ }
+ ],
+ "in": "query",
+ "name": "productRegion",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "description": "This field determines the region where to ship the product based on the value in the shippingPostalCode.",
+ "in": "query",
+ "name": "shippingRegion",
+ "required": false,
+ "type": "string"
+ },
+ {
+ "description": "The locale of the request, in the form of an IETF language tag. Each ProductRegion supports one or more locales. This value should comply with java.util.Locale.",
+ "pattern": "^[-_]+$",
+ "in": "query",
+ "name": "locale",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "description": "The shipping postal or zip code for customer's request. This parameter doesn't hold geocode.",
+ "in": "query",
+ "name": "shippingPostalCode",
+ "required": false,
+ "type": "string"
+ },
+ {
+ "description": "The zero-based number of the page being requested. If not specified, a default value of 0 will be used. When passed, the value must be equal or greater than zero, and less than the number of pages returned in the response.",
+ "in": "query",
+ "name": "pageNumber",
+ "required": false,
+ "type": "integer"
+ },
+ {
+ "description": "The number of items desired for each page in the response. If not specified, a default value of 24 will be used. Maximum items that can be fetched in single request is 24.",
+ "in": "query",
+ "name": "pageSize",
+ "required": false,
+ "type": "integer"
+ },
+ {
+ "description": "A value that identifies the group within the business account that a customer belongs to. The customer can set this in Amazon Business account information. GroupTag is necessary only if the customer account belongs to more than one group.",
+ "in": "query",
+ "name": "groupTag",
+ "required": false,
+ "type": "string"
+ },
+ {
+ "collectionFormat": "csv",
+ "description": "A list of filter ids for use to query results.",
+ "in": "query",
+ "name": "filterIds",
+ "required": false,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "description": "The number of units the customer intends to purchase. This helps Amazon Business determines quantity-based discounts if an eligible offer is present. Defaults to 1.",
+ "in": "query",
+ "name": "quantity",
+ "required": false,
+ "type": "integer",
+ "default": 1
+ },
+ {
+ "description": "The email address of the customer requesting this resource",
+ "in": "header",
+ "name": "x-amz-user-email",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "collectionFormat": "csv",
+ "description": "A list specifying the offer fields for inclusion in the response object. Excluding this query parameter results in the response including all inclusions.",
+ "enum": [
+ "ALL",
+ "availability",
+ "buyingGuidance",
+ "buyingRestrictions",
+ "condition",
+ "fulfillmentType",
+ "merchant",
+ "price",
+ "productCondition",
+ "productConditionNote",
+ "quantityLimits",
+ "quantityPrice",
+ "taxExclusivePrice",
+ "deliveryInformation",
+ "listPrice",
+ "shippingOptions"
+ ],
+ "in": "query",
+ "name": "inclusionsForOffers",
+ "required": false,
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "x-example": "/path?inclusionsForOffers=FOO,BAR"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Search success",
+ "schema": {
+ "$ref": "#/definitions/OffersResult"
+ },
+ "headers": {
+ "x-amzn-RateLimit-Limit": {
+ "description": "The rate limit (requests per second) for this operation.",
+ "type": "string"
+ },
+ "x-amzn-requestid": {
+ "description": "A unique request reference identifier.",
+ "type": "string"
+ }
+ },
+ "x-amazon-spds-sandbox-behaviors": [
+ {
+ "request": {
+ "parameters": {
+ "productRegion": {
+ "value": "DE"
+ },
+ "locale": {
+ "value": "es_US"
+ },
+ "productId": {
+ "value": "B07VDJ5RVF"
+ },
+ "pageNumber": {
+ "value": "1"
+ },
+ "pageSize": {
+ "value": "12"
+ },
+ "filterIds": {
+ "value": ["NEW", "PRIME"]
+ }
+ }
+ },
+ "response": {
+ "offerCount": 2,
+ "numberOfPages": 1,
+ "featuredOffer": {
+ "availability": "[availability]",
+ "buyingGuidance": "NONE",
+ "deliveryInformation": "[deliveryInformation]",
+ "fulfillmentType": "AMAZON_FULFILLMENT",
+ "offerId": "[offerId]",
+ "merchant": {
+ "name": "[merchantName]",
+ "merchantId": "[merchantId]",
+ "meanFeedbackRating": 5.0,
+ "totalFeedbackCount": 1
+ },
+ "price": {
+ "value": {
+ "amount": 1.0,
+ "currencyCode": "USD"
+ },
+ "formattedPrice": "[formattedPrice]",
+ "priceType": "NEW"
+ },
+ "listPrice": {
+ "value": {
+ "amount": 2.0,
+ "currencyCode": "USD"
+ },
+ "formattedPrice": "[formattedPrice]",
+ "priceType": "NEW"
+ },
+ "taxExclusivePrice": {
+ "taxExclusiveAmount": {
+ "amount": 1.0,
+ "currencyCode": "USD"
+ },
+ "formattedPrice": "[formattedPrice]",
+ "displayString": "[taxExclusivePriceDisplayString]",
+ "label": "[taxExclusivePriceLabel]"
+ },
+ "quantityLimits": {
+ "minQuantity": 1,
+ "maxQuantity": 6
+ },
+ "quantityPrice": {
+ "quantityPriceTiers": [
+ {
+ "quantityDisplay": "string",
+ "unitPrice": {
+ "amount": 0,
+ "currencyCode": "USD"
+ },
+ "minQuantity": 0,
+ "price": {
+ "value": {
+ "amount": 0,
+ "currencyCode": "USD"
+ },
+ "formattedPrice": "[formattedPrice]",
+ "priceType": "NEW"
+ },
+ "savingMessage": "string",
+ "taxExclusivePrice": {
+ "taxExclusiveAmount": {
+ "amount": 0,
+ "currencyCode": "USD"
+ },
+ "displayString": "[taxExclusivePriceDisplayString]",
+ "formattedPrice": "[formattedPrice]",
+ "label": "[taxExclusivePriceLabel]"
+ }
+ }
+ ]
+ },
+ "productCondition": "NEW",
+ "productConditionNote": "[productConditionNote]",
+ "condition": {
+ "conditionValue": "NEW",
+ "conditionNote": "string",
+ "subCondition": "ACCEPTABLE"
+ },
+ "buyingRestrictions": [],
+ "shippingOptions": [
+ {
+ "shippingCost": {
+ "value": {
+ "amount": 9.0,
+ "currencyCode": "USD"
+ }
+ },
+ "deliveryRange": {
+ "max": "2022-01-01T00:00:01.000Z",
+ "min": "2022-01-01T00:00:01.000Z"
+ }
+ }
+ ]
+ },
+ "offers": [
+ {
+ "offerId": "[offerId]",
+ "merchant": {
+ "name": "[merchantName]",
+ "merchantId": "[merchantId]",
+ "meanFeedbackRating": 5.0,
+ "totalFeedbackCount": 1
+ },
+ "deliveryInformation": "[deliveryInformation]",
+ "price": {
+ "value": {
+ "amount": 1.0,
+ "currencyCode": "USD"
+ },
+ "formattedPrice": "[formattedPrice]",
+ "priceType": "NEW"
+ },
+ "listPrice": {
+ "value": {
+ "amount": 2.0,
+ "currencyCode": "USD"
+ },
+ "formattedPrice": "[formattedPrice]",
+ "priceType": "NEW"
+ },
+ "taxExclusivePrice": {
+ "taxExclusiveAmount": {
+ "amount": 1.0,
+ "currencyCode": "USD"
+ },
+ "formattedPrice": "[formattedPrice]",
+ "displayString": "[taxExclusivePriceDisplayString]",
+ "label": "[taxExclusivePriceLabel]"
+ },
+ "quantityLimits": {
+ "minQuantity": 1,
+ "maxQuantity": 6
+ },
+ "quantityPrice": {
+ "quantityPriceTiers": [
+ {
+ "quantityDisplay": "string",
+ "unitPrice": {
+ "amount": 0,
+ "currencyCode": "USD"
+ },
+ "minQuantity": 0,
+ "price": {
+ "value": {
+ "amount": 0,
+ "currencyCode": "USD"
+ },
+ "formattedPrice": "[formattedPrice]",
+ "priceType": "NEW"
+ },
+ "savingMessage": "string",
+ "taxExclusivePrice": {
+ "taxExclusiveAmount": {
+ "amount": 0,
+ "currencyCode": "USD"
+ },
+ "displayString": "[taxExclusivePriceDisplayString]",
+ "formattedPrice": "[formattedPrice]",
+ "label": "[taxExclusivePriceLabel]"
+ }
+ }
+ ]
+ },
+ "productCondition": "NEW",
+ "productConditionNote": "[productConditionNote]",
+ "condition": {
+ "conditionValue": "NEW",
+ "conditionNote": "string",
+ "subCondition": "ACCEPTABLE"
+ },
+ "availability": "[availability]",
+ "fulfillmentType": "THIRD_PARTY",
+ "buyingGuidance": "NONE",
+ "buyingRestrictions": [],
+ "shippingOptions": [
+ {
+ "shippingCost": {
+ "value": {
+ "amount": 9.0,
+ "currencyCode": "USD"
+ }
+ },
+ "deliveryRange": {
+ "max": "2022-01-01T00:00:01.000Z",
+ "min": "2022-01-01T00:00:01.000Z"
+ }
+ }
+ ]
+ }
+ ],
+ "filterGroups": [
+ {
+ "displayName": "[displayName]",
+ "items": [
+ {
+ "displayName": "[itemDisplayName]",
+ "id": "[itemId]"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "400": {
+ "description": "The service was unable to process the request. Reasons for the error are described in an error response object.",
+ "schema": {
+ "$ref": "#/definitions/ErrorResponse"
+ },
+ "headers": {
+ "x-amzn-RateLimit-Limit": {
+ "description": "The rate limit (requests per second) for this operation.",
+ "type": "string"
+ },
+ "x-amzn-requestid": {
+ "description": "A unique request reference identifier.",
+ "type": "string"
+ }
+ },
+ "x-amazon-spds-sandbox-behaviors": [
+ {
+ "request": {
+ "parameters": {
+ "productRegion": {
+ "value": "US"
+ },
+ "productId": {
+ "value": "B07VDJ5RVF"
+ }
+ }
+ },
+ "response": {
+ "errors": [
+ {
+ "code": "INVALID_REQUEST_PARAMETER",
+ "message": "Missing required request parameters: [locale]"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "401": {
+ "description": "The request's authorization header isn't formatted correctly or doesn't contain a valid authorization input.",
+ "schema": {
+ "$ref": "#/definitions/ErrorResponse"
+ },
+ "headers": {
+ "x-amzn-RateLimit-Limit": {
+ "description": "The rate limit (requests per second) for this operation.",
+ "type": "string"
+ },
+ "x-amzn-requestid": {
+ "description": "A unique request reference identifier.",
+ "type": "string"
+ }
+ }
+ },
+ "403": {
+ "description": "Indicates that access to the resource is forbidden. Possible reasons include Access Denied, Unauthorized, Expired Token, or Invalid Signature.",
+ "schema": {
+ "$ref": "#/definitions/ErrorResponse"
+ },
+ "headers": {
+ "x-amzn-RateLimit-Limit": {
+ "description": "The rate limit (requests per second) for this operation.",
+ "type": "string"
+ },
+ "x-amzn-requestid": {
+ "description": "A unique request reference identifier.",
+ "type": "string"
+ }
+ }
+ },
+ "404": {
+ "description": "The resource specified doesn't exist.",
+ "schema": {
+ "$ref": "#/definitions/ErrorResponse"
+ },
+ "headers": {
+ "x-amzn-RateLimit-Limit": {
+ "description": "The rate limit (requests per second) for this operation.",
+ "type": "string"
+ },
+ "x-amzn-requestid": {
+ "description": "A unique request reference identifier.",
+ "type": "string"
+ }
+ }
+ },
+ "429": {
+ "description": "The frequency of requests was greater than allowed.",
+ "schema": {
+ "$ref": "#/definitions/ErrorResponse"
+ },
+ "headers": {
+ "x-amzn-RateLimit-Limit": {
+ "description": "The rate limit (requests per second) for this operation.",
+ "type": "string"
+ },
+ "x-amzn-requestid": {
+ "description": "A unique request reference identifier.",
+ "type": "string"
+ }
+ }
+ },
+ "500": {
+ "description": "Encountered an unexpected condition which prevented the server from fulfilling the request.",
+ "schema": {
+ "$ref": "#/definitions/ErrorResponse"
+ },
+ "headers": {
+ "x-amzn-RateLimit-Limit": {
+ "description": "The rate limit (requests per second) for this operation.",
+ "type": "string"
+ },
+ "x-amzn-requestid": {
+ "description": "A unique request reference identifier.",
+ "type": "string"
+ }
+ }
+ },
+ "503": {
+ "description": "Temporary overloading or maintenance of the server.",
+ "schema": {
+ "$ref": "#/definitions/ErrorResponse"
+ },
+ "headers": {
+ "x-amzn-RateLimit-Limit": {
+ "description": "The rate limit (requests per second) for this operation.",
+ "type": "string"
+ },
+ "x-amzn-requestid": {
+ "description": "A unique request reference identifier.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "tags": ["search"],
+ "description": "Search for offers of a specific product.",
+ "operationId": "searchOffersRequest",
+ "summary": "Paginated and filtered search for offers of a specific product."
+ }
+ }
+}
diff --git a/config/custom-checkstyle.xml b/config/custom-checkstyle.xml
new file mode 100644
index 0000000..71d9e97
--- /dev/null
+++ b/config/custom-checkstyle.xml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config/demo.config.properties b/config/demo.config.properties
new file mode 100644
index 0000000..fbb6e44
--- /dev/null
+++ b/config/demo.config.properties
@@ -0,0 +1,3 @@
+# Amazon Credentials
+AMAZON_API_URL=
+AMAZON_API_KEY=
\ No newline at end of file
diff --git a/config/logback.xml b/config/logback.xml
new file mode 100644
index 0000000..6b610a9
--- /dev/null
+++ b/config/logback.xml
@@ -0,0 +1,12 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+ UTF-8
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 880badf..88f5f8e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,27 +5,58 @@
de.rwu
easydrop
jar
- 0.1-SNAPSHOT
+ 0.1.0
EasyDrop
http://maven.apache.org
UTF-8
+ 17
+ 17
- junit
- junit
- 4.13.2
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.8.0
test
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.8.0
+ test
+
+
org.mockito
mockito-core
5.3.1
test
+
+
+ org.json
+ json
+ 20230227
+
+
+ com.jayway.jsonpath
+ json-path
+ 2.8.0
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.7
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.7
+
@@ -39,6 +70,7 @@
checkstyle-check
verify
+ config/custom-checkstyle.xml
true
true
true
diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java
new file mode 100644
index 0000000..af1aeea
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/Main.java
@@ -0,0 +1,49 @@
+package de.rwu.easydrop;
+
+import javax.naming.ConfigurationException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.rwu.easydrop.api.client.AmazonProductDataSource;
+import de.rwu.easydrop.util.Config;
+
+/**
+ * Kickoff point for the service.
+ *
+ * @since 0.1.0
+ */
+public final class Main {
+ /**
+ * Logger for main process.
+ */
+ private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
+
+ /**
+ * Prevents unwanted instantiation.
+ */
+ private Main() throws IllegalAccessException {
+ throw new IllegalAccessException("Don't instantiate me! >:(");
+ }
+
+ /**
+ * Application entrypoint.
+ *
+ * @param args
+ */
+ public static void main(final String[] args) throws ConfigurationException {
+ Config config = Config.getInstance();
+ config.loadConfig();
+ String amznBaseUrl = config.getProperty("AMAZON_API_URL");
+ String amznApiKey = config.getProperty("AMAZON_API_KEY");
+ String testProduct = null;
+
+ AmazonProductDataSource amznSrc = new AmazonProductDataSource(amznBaseUrl, amznApiKey);
+ try {
+ testProduct = amznSrc.getProductDTOById("B096Y2TYKV").toString();
+ LOGGER.info(testProduct);
+ } catch (IllegalArgumentException e) {
+ LOGGER.error("Something went wrong :(", e);
+ }
+ }
+}
diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java
new file mode 100644
index 0000000..dbbdc6d
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java
@@ -0,0 +1,125 @@
+package de.rwu.easydrop.api.client;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import com.jayway.jsonpath.JsonPath;
+import com.jayway.jsonpath.PathNotFoundException;
+import com.jayway.jsonpath.ReadContext;
+
+import de.rwu.easydrop.api.dto.ProductDTO;
+
+/**
+ * Interface to an Amazon data source.
+ *
+ * @since 0.1.0
+ */
+public final class AmazonProductDataSource implements DataSource {
+ /**
+ * Name of this data source.
+ */
+ private static final String DATA_ORIGIN = "Amazon";
+ /**
+ * Base URL to the Amazon data source.
+ */
+ private String baseUrl;
+ /**
+ * Credential key to authorize acccess.
+ */
+ private String apiKey;
+ /**
+ * Product region parameter required for data access.
+ */
+ private static final String PRODUCT_REGION = "DE";
+ /**
+ * Locale parameter required for data access.
+ */
+ private static final String LOCALE = "de_DE";
+
+ /**
+ * Sets up instance with Base URL and API Key.
+ *
+ * @param newBaseUrl
+ * @param newApiKey
+ */
+ public AmazonProductDataSource(final String newBaseUrl, final String newApiKey) {
+ this.baseUrl = newBaseUrl;
+ this.apiKey = newApiKey;
+ }
+
+ @Override
+ public ProductDTO getProductDTOById(final String productId) throws IllegalArgumentException {
+ StringBuilder response = new StringBuilder();
+ ProductDTO product = new ProductDTO(productId, DATA_ORIGIN);
+
+ try {
+ URL apiUrl = createApiUrl(productId);
+
+ HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setRequestProperty("Credential", apiKey);
+
+ int responseCode = connection.getResponseCode();
+ BufferedReader reader;
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+ } else {
+ throw new IllegalArgumentException(
+ "Nothing found: Amazon API responded with error code " + responseCode);
+ }
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ response.append(line);
+ }
+ reader.close();
+
+ buildProductDTO(product, response.toString());
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Couldn't fulfill Amazon API request");
+ }
+
+ return product;
+ }
+
+ /**
+ * Enriches a ProductDTO with API-gathered data.
+ *
+ * @param product Unfinished ProductDTO
+ * @param json Product data
+ * @return Finished ProductDTO
+ */
+ public ProductDTO buildProductDTO(final ProductDTO product, final String json) {
+ String root = "$.featuredOffer.";
+ ReadContext ctx = JsonPath.parse(json);
+
+ try {
+ product.setDataOrigin(DATA_ORIGIN);
+ product.setAvailable(
+ ctx.read(root + "availability", String.class).equals("available"));
+ product.setCurrentPrice(ctx.read(root + "price.value.amount", double.class));
+ product.setDeliveryPrice(
+ ctx.read(root + "shippingOptions[0].shippingCost.value.amount", double.class));
+ product.setMerchant(ctx.read(root + "merchant.name", String.class));
+ } catch (PathNotFoundException e) {
+ // Pass, allow incomplete ProductDTO to pass for later validation
+ }
+
+ return product;
+ }
+
+ @Override
+ public URL createApiUrl(final String productId) throws MalformedURLException {
+ return new URL(baseUrl
+ + "/products/2020-08-26/products/"
+ + productId
+ + "/offers?productRegion="
+ + PRODUCT_REGION
+ + "&locale="
+ + LOCALE);
+ }
+}
diff --git a/src/main/java/de/rwu/easydrop/api/client/DataSource.java b/src/main/java/de/rwu/easydrop/api/client/DataSource.java
new file mode 100644
index 0000000..7b1c4e7
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/api/client/DataSource.java
@@ -0,0 +1,28 @@
+package de.rwu.easydrop.api.client;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import de.rwu.easydrop.api.dto.ProductDTO;
+
+/**
+ * Universal interface to implement concrete APIs on.
+ */
+public interface DataSource {
+ /**
+ * Retrieves product info from the data source.
+ *
+ * @param productId ASIN
+ * @return ProductDTO
+ */
+ ProductDTO getProductDTOById(String productId);
+
+ /**
+ * Creates an URL object to connect to the API with.
+ *
+ * @param productId ASIN
+ * @return URL object
+ * @throws MalformedURLException
+ */
+ URL createApiUrl(String productId) throws MalformedURLException;
+}
diff --git a/src/main/java/de/rwu/easydrop/api/client/package-info.java b/src/main/java/de/rwu/easydrop/api/client/package-info.java
new file mode 100644
index 0000000..b4040d6
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/api/client/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * API client connectors for interaction with external data sources.
+ *
+ * @since 0.1.0
+ */
+package de.rwu.easydrop.api.client;
diff --git a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java
new file mode 100644
index 0000000..e3a706c
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java
@@ -0,0 +1,139 @@
+package de.rwu.easydrop.api.dto;
+
+import de.rwu.easydrop.util.FormattingUtil;
+
+/**
+ * Product data transfer object.
+ *
+ * @since 0.1.0
+ */
+public class ProductDTO {
+ /**
+ * Data source platform, like "Amazon".
+ */
+ private String dataOrigin;
+
+ /**
+ * @return the dataOrigin
+ */
+ public String getDataOrigin() {
+ return dataOrigin;
+ }
+
+ /**
+ * @param newDataOrigin the dataOrigin to set
+ */
+ public void setDataOrigin(final String newDataOrigin) {
+ this.dataOrigin = newDataOrigin;
+ }
+
+ /**
+ * Platform internal product identifier.
+ */
+ private String productId;
+
+ /**
+ * @return the productId
+ */
+ public String getProductId() {
+ return productId;
+ }
+
+ /**
+ * Current product price per piece in Euro.
+ */
+ private double currentPrice;
+
+ /**
+ * @return the currentPrice
+ */
+ public double getCurrentPrice() {
+ return currentPrice;
+ }
+
+ /**
+ * @param newCurrentPrice the currentPrice to set
+ */
+ public void setCurrentPrice(final double newCurrentPrice) {
+ this.currentPrice = newCurrentPrice;
+ }
+
+ /**
+ * Name of mercant offering the product on the platform.
+ */
+ private String merchant;
+
+ /**
+ * @return the merchant
+ */
+ public String getMerchant() {
+ return merchant;
+ }
+
+ /**
+ * @param newMerchant the merchant to set
+ */
+ public void setMerchant(final String newMerchant) {
+ this.merchant = newMerchant;
+ }
+
+ /**
+ * Additional Cost for delivery in Euro.
+ */
+ private double deliveryPrice;
+
+ /**
+ * @return the deliveryPrice
+ */
+ public double getDeliveryPrice() {
+ return deliveryPrice;
+ }
+
+ /**
+ * @param newDeliveryPrice the deliveryPrice to set
+ */
+ public void setDeliveryPrice(final double newDeliveryPrice) {
+ this.deliveryPrice = newDeliveryPrice;
+ }
+
+ /**
+ * Whether the product can be purchased at this point.
+ */
+ private boolean available;
+
+ /**
+ * @return the available
+ */
+ public boolean isAvailable() {
+ return available;
+ }
+
+ /**
+ * @param newAvailable the available to set
+ */
+ public void setAvailable(final boolean newAvailable) {
+ this.available = newAvailable;
+ }
+
+ /**
+ * Creates ProductDTO instance.
+ *
+ * @param newProductId Interal Product indetifier
+ * @param newDataOrigin Data Origin
+ */
+ public ProductDTO(final String newProductId, final String newDataOrigin) {
+ this.productId = newProductId;
+ this.dataOrigin = newDataOrigin;
+ }
+
+ @Override
+ public final String toString() {
+ return "ProductDTO{"
+ + productId + " from "
+ + merchant + " ("
+ + dataOrigin + ")"
+ + " at "
+ + FormattingUtil.formatEuro(currentPrice) + " (available: "
+ + (available ? "yes" : "no") + ")}";
+ }
+}
diff --git a/src/main/java/de/rwu/easydrop/api/dto/package-info.java b/src/main/java/de/rwu/easydrop/api/dto/package-info.java
new file mode 100644
index 0000000..7d4f621
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/api/dto/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Data transfer objects for data derived from external sources.
+ *
+ * @since 0.1.0
+ */
+package de.rwu.easydrop.api.dto;
diff --git a/src/main/java/de/rwu/easydrop/api/package-info.java b/src/main/java/de/rwu/easydrop/api/package-info.java
new file mode 100644
index 0000000..5c0f1cf
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/api/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Interaction with external APIs.
+ *
+ * @since 0.1.0
+ */
+package de.rwu.easydrop.api;
diff --git a/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java b/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java
new file mode 100644
index 0000000..ce6360a
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java
@@ -0,0 +1,10 @@
+package de.rwu.easydrop.data.connector;
+
+/**
+ * Allows connecting to a SQLite Database.
+ *
+ * TODO implement
+ */
+public class DatabaseConnector {
+
+}
diff --git a/src/main/java/de/rwu/easydrop/data/connector/package-info.java b/src/main/java/de/rwu/easydrop/data/connector/package-info.java
new file mode 100644
index 0000000..f5b2dc7
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/data/connector/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Connectors for databases.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.data.connector;
diff --git a/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java b/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java
new file mode 100644
index 0000000..f787da9
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java
@@ -0,0 +1,10 @@
+package de.rwu.easydrop.data.dao;
+
+/**
+ * Product data access object.
+ *
+ * TODO implement
+ */
+public class ProductDAO {
+
+}
diff --git a/src/main/java/de/rwu/easydrop/data/dao/package-info.java b/src/main/java/de/rwu/easydrop/data/dao/package-info.java
new file mode 100644
index 0000000..2affd23
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/data/dao/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Data access objects for business objects created from persistence.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.data.dao;
diff --git a/src/main/java/de/rwu/easydrop/data/model/Product.java b/src/main/java/de/rwu/easydrop/data/model/Product.java
new file mode 100644
index 0000000..cfee872
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/data/model/Product.java
@@ -0,0 +1,10 @@
+package de.rwu.easydrop.data.model;
+
+/**
+ * A Product.
+ *
+ * TODO implement
+ */
+public class Product {
+
+}
diff --git a/src/main/java/de/rwu/easydrop/data/model/package-info.java b/src/main/java/de/rwu/easydrop/data/model/package-info.java
new file mode 100644
index 0000000..f33ce31
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/data/model/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Business objects.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.data.model;
diff --git a/src/main/java/de/rwu/easydrop/data/package-info.java b/src/main/java/de/rwu/easydrop/data/package-info.java
new file mode 100644
index 0000000..a161322
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/data/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Structure for business objects and persisting their info.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.data;
diff --git a/src/main/java/de/rwu/easydrop/demo/DemoClass.java b/src/main/java/de/rwu/easydrop/demo/DemoClass.java
deleted file mode 100644
index ff91a4f..0000000
--- a/src/main/java/de/rwu/easydrop/demo/DemoClass.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.rwu.easydrop.demo;
-
-/**
- * Demo-Class.
- */
-public final class DemoClass {
- /**
- * Demo-Method inverting a boolean.
- *
- * @param bool Input Boolean
- * @return Inverted Boolean
- */
- public boolean invertBool(final boolean bool) {
- return !bool;
- }
-}
diff --git a/src/main/java/de/rwu/easydrop/demo/package-info.java b/src/main/java/de/rwu/easydrop/demo/package-info.java
deleted file mode 100644
index 01c1a1b..0000000
--- a/src/main/java/de/rwu/easydrop/demo/package-info.java
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * Dieses Paket beinhaltet ist zu Demo-Zwecken.
- *
- * @since 0.1-SNAPSHOT
- */
-package de.rwu.easydrop.demo;
diff --git a/src/main/java/de/rwu/easydrop/package-info.java b/src/main/java/de/rwu/easydrop/package-info.java
new file mode 100644
index 0000000..9d9a0d1
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * One of the Top 2 dropshipping platforms around.
+ *
+ * @since 0.1.0
+ */
+package de.rwu.easydrop;
diff --git a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java
new file mode 100644
index 0000000..5d4a6d3
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java
@@ -0,0 +1,14 @@
+package de.rwu.easydrop.service.mapping;
+
+/**
+ * Maps between Product, ProductDAO and ProductDTO.
+ *
+ * TODO implement
+ *
+ * @see Product
+ * @see ProductDTO
+ * @see ProductDAO
+ */
+public class ProductMapper {
+
+}
diff --git a/src/main/java/de/rwu/easydrop/service/mapping/package-info.java b/src/main/java/de/rwu/easydrop/service/mapping/package-info.java
new file mode 100644
index 0000000..ab86051
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/service/mapping/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Maps different formats of corresponding objects.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.service.mapping;
diff --git a/src/main/java/de/rwu/easydrop/service/package-info.java b/src/main/java/de/rwu/easydrop/service/package-info.java
new file mode 100644
index 0000000..ff59e0c
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/service/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Packages for supporting business logic.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.service;
diff --git a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java
new file mode 100644
index 0000000..4d373b2
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java
@@ -0,0 +1,10 @@
+package de.rwu.easydrop.service.processing;
+
+/**
+ * Processes dropshipping orders.
+ *
+ * TODO implement
+ */
+public class OrderManager {
+
+}
diff --git a/src/main/java/de/rwu/easydrop/service/processing/package-info.java b/src/main/java/de/rwu/easydrop/service/processing/package-info.java
new file mode 100644
index 0000000..db7cb9e
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/service/processing/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Supports diverse business processes and enforces business rules.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.service.processing;
diff --git a/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java
new file mode 100644
index 0000000..3c93108
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java
@@ -0,0 +1,10 @@
+package de.rwu.easydrop.service.validation;
+
+/**
+ * Confirms validity of Product data.
+ *
+ * @since 0.1.0
+ */
+public class ProductValidator {
+
+}
diff --git a/src/main/java/de/rwu/easydrop/service/validation/package-info.java b/src/main/java/de/rwu/easydrop/service/validation/package-info.java
new file mode 100644
index 0000000..6b45561
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/service/validation/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Supports validation processes.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.service.validation;
diff --git a/src/main/java/de/rwu/easydrop/util/Config.java b/src/main/java/de/rwu/easydrop/util/Config.java
new file mode 100644
index 0000000..7a7f1e3
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/util/Config.java
@@ -0,0 +1,112 @@
+package de.rwu.easydrop.util;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.NoSuchElementException;
+import java.util.Properties;
+
+import javax.naming.ConfigurationException;
+
+/**
+ * Allows access to the config.
+ *
+ * @since 0.1.0
+ */
+public final class Config {
+ /**
+ * Config file location.
+ */
+ private String configLocation = "config/config.properties";
+
+ /**
+ * @return the configLocation
+ */
+ public String getConfigLocation() {
+ return configLocation;
+ }
+
+ /**
+ * @param newConfigLocation the configLocation to set
+ */
+ public void setConfigLocation(final String newConfigLocation) {
+ configLocation = newConfigLocation;
+ }
+
+ /**
+ * Holds the config values.
+ */
+ private Properties properties = null;
+ /**
+ * Singleton instance.
+ */
+ private static Config instance = null;
+
+ /**
+ * Private constructor to prevent external instantiation.
+ */
+ private Config() {
+ // Do Nothing
+ }
+
+ /**
+ * Returns current config instance.
+ *
+ * @return Config instance
+ * @throws ConfigurationException
+ */
+ public static Config getInstance() {
+ if (instance == null) {
+ return new Config();
+ }
+
+ return instance;
+ }
+
+ /**
+ * Loads config file values into the instance.
+ *
+ * @throws ConfigurationException
+ */
+ public void loadConfig() throws ConfigurationException {
+ Properties newProps = new Properties();
+ try (FileInputStream input = new FileInputStream(configLocation)) {
+ newProps.load(input);
+ properties = newProps;
+ } catch (IOException e) {
+ throw new ConfigurationException("Couldn't load required config file");
+ }
+ }
+
+ /**
+ * Returns a config property by specified key.
+ *
+ * @param key Config Key, like "API_KEY"
+ * @return Config value
+ * @throws NoSuchElementException Required key missing
+ */
+ public String getProperty(final String key) throws NoSuchElementException {
+ String value = null;
+
+ try {
+ value = properties.getProperty(key);
+ } catch (NullPointerException e) {
+ throw new NoSuchElementException("Config has not been loaded");
+ }
+
+ if (value == null) {
+ throw new NoSuchElementException("Requested config value does not exist");
+ }
+
+ return value;
+ }
+
+ /**
+ * Overrides a config property loaded from file for current instance.
+ *
+ * @param key Config Key
+ * @param value Property Value
+ */
+ public void setProperty(final String key, final String value) {
+ properties.setProperty(key, value);
+ }
+}
diff --git a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java
new file mode 100644
index 0000000..283e506
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java
@@ -0,0 +1,30 @@
+package de.rwu.easydrop.util;
+
+import java.util.Locale;
+
+/**
+ * Helps format a bunch of things.
+ *
+ * @since 0.1.0
+ */
+public final class FormattingUtil {
+
+ /**
+ * Private constructor to prevent unwanted instantiation.
+ *
+ * @throws UnsupportedOperationException always
+ */
+ private FormattingUtil() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("This is a utility class, don't instantiate it.");
+ }
+
+ /**
+ * Formats a price to the German Euro format.
+ *
+ * @param amount price
+ * @return formatted price
+ */
+ public static String formatEuro(final double amount) {
+ return String.format(Locale.GERMAN, "%,.2f", amount) + " €";
+ }
+}
diff --git a/src/main/java/de/rwu/easydrop/util/package-info.java b/src/main/java/de/rwu/easydrop/util/package-info.java
new file mode 100644
index 0000000..40789f2
--- /dev/null
+++ b/src/main/java/de/rwu/easydrop/util/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * General utility such as formatting helpers.
+ *
+ * TODO implement
+ */
+package de.rwu.easydrop.util;
diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java
new file mode 100644
index 0000000..032d682
--- /dev/null
+++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java
@@ -0,0 +1,189 @@
+package de.rwu.easydrop.api.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockitoAnnotations;
+
+import de.rwu.easydrop.api.dto.ProductDTO;
+
+class AmazonProductDataSourceTest {
+
+ private AmazonProductDataSource demoDataSource;
+
+ private static String demoApiKey = "my-api-key";
+ private static String demoApiUrl = "https://www.example.com/api";
+ private static String demoDataOrigin = "Amazon";
+ private static String demoProductId = "whateverId";
+
+ @BeforeEach
+ void setup() {
+ demoDataSource = new AmazonProductDataSource(
+ demoApiUrl,
+ demoApiKey);
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ void testConstructor() {
+ // Assert
+ try {
+ Field baseUrlField = AmazonProductDataSource.class.getDeclaredField("baseUrl");
+ baseUrlField.setAccessible(true);
+ Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoDataSource));
+
+ Field apiKeyField = AmazonProductDataSource.class.getDeclaredField("apiKey");
+ apiKeyField.setAccessible(true);
+ Assertions.assertEquals(demoApiKey, apiKeyField.get(demoDataSource));
+ } catch (NoSuchFieldException e) {
+ Assertions.fail();
+ } catch (IllegalAccessException e) {
+ Assertions.fail();
+ }
+ }
+
+ @Test
+ void testCreateApiUrl() throws MalformedURLException {
+ // Test case 1
+ String productId1 = "12345";
+ URL expectedUrl1 = new URL(
+ "https://www.example.com/api/products/2020-08-26/products/12345/offers?productRegion=DE&locale=de_DE");
+ URL createdUrl1 = demoDataSource.createApiUrl(productId1);
+ Assertions.assertEquals(expectedUrl1, createdUrl1);
+
+ // Test case 2
+ String productId2 = "67890";
+ URL expectedUrl2 = new URL(
+ "https://www.example.com/api/products/2020-08-26/"
+ + "products/67890/offers?productRegion=DE&locale=de_DE");
+ URL createdUrl2 = demoDataSource.createApiUrl(productId2);
+ Assertions.assertEquals(expectedUrl2, createdUrl2);
+ }
+
+ @Test
+ void testBuildProductDTO_completeData() {
+ // Set up the test environment
+ String json = "{\"featuredOffer\": {\"availability\": \"available\"," +
+ "\"price\": {\"value\": {\"amount\": 10.0}}," +
+ "\"shippingOptions\": [{\"shippingCost\": {\"value\": {\"amount\": 2.5}}}], " +
+ "\"merchant\": {\"name\": \"Merchant A\"}}}";
+ ProductDTO product = new ProductDTO(demoProductId, demoDataOrigin);
+
+ // Invoke the method
+ ProductDTO result = demoDataSource.buildProductDTO(product, json);
+
+ // Verify the product DTO properties
+ assertEquals(demoDataOrigin, result.getDataOrigin());
+ assertEquals(true, result.isAvailable());
+ assertEquals(10.0, result.getCurrentPrice());
+ assertEquals(2.5, result.getDeliveryPrice());
+ assertEquals("Merchant A", result.getMerchant());
+ }
+
+ @Test
+ void testBuildProductDTO_incompleteData() {
+ // Set up the test environment
+ String json = "{\"otherOffer\": {\"availability\": \"available\"," +
+ "\"price\": {\"value\": {\"amount\": 10.0}}," +
+ "\"shippingOptions\": [{\"shippingCost\": {\"value\": {\"amount\": 2.5}}}], " +
+ "\"merchant\": {\"name\": \"Merchant A\"}}}";
+ ProductDTO product = new ProductDTO(demoProductId, demoDataOrigin);
+
+ // Invoke the method
+ ProductDTO result = demoDataSource.buildProductDTO(product, json);
+
+ // Verify that the product DTO remains unchanged
+ assertEquals(demoDataOrigin, result.getDataOrigin());
+ assertEquals(false, result.isAvailable());
+ assertEquals(0.0, result.getCurrentPrice());
+ assertEquals(0.0, result.getDeliveryPrice());
+ assertEquals(null, result.getMerchant());
+ }
+
+ @Test
+ void testGetProductDTOById_successfulRequest() throws IOException {
+ // Set up the test environment
+ String mockResponse = "{\"featuredOffer\": {\"availability\": \"available\"," +
+ "\"price\": {\"value\": {\"amount\": 10.0}}," +
+ "\"shippingOptions\": [{\"shippingCost\": {\"value\": {\"amount\": 2.5}}}], " +
+ "\"merchant\": {\"name\": \"Merchant A\"}}}";
+
+ AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class);
+ URL mockURL = mock(URL.class);
+ when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL);
+ when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod();
+ HttpURLConnection mockConnection = mock(HttpURLConnection.class);
+ when(mockURL.openConnection()).thenReturn(mockConnection);
+ when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ when(mockConnection.getInputStream()).thenReturn(new ByteArrayInputStream(mockResponse.getBytes()));
+ when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod();
+
+ // Invoke the method
+ ProductDTO result = dataSource.getProductDTOById(demoProductId);
+
+ // Verify the product DTO properties
+ assertEquals(demoProductId, result.getProductId());
+ assertEquals("Amazon", result.getDataOrigin());
+ assertEquals(true, result.isAvailable());
+ assertEquals(10.0, result.getCurrentPrice());
+ assertEquals(2.5, result.getDeliveryPrice());
+ assertEquals("Merchant A", result.getMerchant());
+ }
+
+ @Test
+ void testGetProductDTOById_failedRequest() throws IOException {
+ // Set up the test environment
+
+ AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class);
+ URL mockURL = mock(URL.class);
+ when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL);
+ when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod();
+ HttpURLConnection mockConnection = mock(HttpURLConnection.class);
+ when(mockURL.openConnection()).thenReturn(mockConnection);
+ when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND);
+
+ // Invoke the method and verify the exception
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ dataSource.getProductDTOById(demoProductId);
+ });
+
+ // Verify the exception message
+ assertEquals("Nothing found: Amazon API responded with error code 404", exception.getMessage());
+ }
+
+ @Test
+ void testGetProductDTOById_ioException() throws IOException {
+ // Set up the test environment
+ AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class);
+ URL mockURL = mock(URL.class);
+ when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL);
+ when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod();
+ when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod();
+ HttpURLConnection mockConnection = mock(HttpURLConnection.class);
+ when(mockURL.openConnection()).thenReturn(mockConnection);
+ when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ when(mockConnection.getInputStream()).thenThrow(new IOException());
+
+ // Invoke the method and verify the exception
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ dataSource.getProductDTOById(demoProductId);
+ });
+
+ // Verify the exception message
+ assertEquals("Couldn't fulfill Amazon API request", exception.getMessage());
+ }
+}
diff --git a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java
new file mode 100644
index 0000000..9d5917a
--- /dev/null
+++ b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java
@@ -0,0 +1,33 @@
+package de.rwu.easydrop.api.dto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class ProductDTOTest {
+ @Test
+ void testToString1() {
+ ProductDTO product1 = new ProductDTO("12345", "Amazon");
+ product1.setMerchant("Merchant A");
+ product1.setCurrentPrice(19.99);
+ product1.setAvailable(true);
+
+ String expectedString1 = "ProductDTO{12345 from Merchant A (Amazon) at 19,99 € (available: yes)}";
+ String result1 = product1.toString();
+
+ assertEquals(expectedString1, result1);
+ }
+
+ @Test
+ void testToString2() {
+ ProductDTO product2 = new ProductDTO("67890", "eBay");
+ product2.setMerchant("Merchant B");
+ product2.setCurrentPrice(9.99);
+ product2.setAvailable(false);
+
+ String expectedString2 = "ProductDTO{67890 from Merchant B (eBay) at 9,99 € (available: no)}";
+ String result2 = product2.toString();
+
+ assertEquals(expectedString2, result2);
+ }
+}
diff --git a/src/test/java/de/rwu/easydrop/demo/DemoClassTest.java b/src/test/java/de/rwu/easydrop/demo/DemoClassTest.java
deleted file mode 100644
index fb4aa0e..0000000
--- a/src/test/java/de/rwu/easydrop/demo/DemoClassTest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.rwu.easydrop.demo;
-
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
-
-public class DemoClassTest {
- private final DemoClass demoClass = new DemoClass();
-
- @Test
- public void testInvertBoolTrue() {
- assertEquals("Inverting true should return false", false, demoClass.invertBool(true));
- }
-
- @Test
- public void testInvertBoolFalse() {
- assertEquals("Inverting false should return true", true, demoClass.invertBool(false));
- }
-}
diff --git a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java
new file mode 100644
index 0000000..db7de79
--- /dev/null
+++ b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java
@@ -0,0 +1,95 @@
+package de.rwu.easydrop.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.NoSuchElementException;
+
+import javax.naming.ConfigurationException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ConfigImplTest {
+ private Config config;
+ private final static String TESTDATA_PATH = "testResources/testdata.properties";
+ private final static String TESTDATA_KEY = "API_KEY";
+ private final static String TESTDATA_VAL = "keyIsHere";
+
+ @BeforeEach
+ void setUp() {
+ config = Config.getInstance();
+ }
+
+ @Test
+ void testGetProperty_ExistingKey() throws ConfigurationException {
+ config.setConfigLocation(TESTDATA_PATH);
+ config.loadConfig();
+ config.setProperty(TESTDATA_KEY, TESTDATA_VAL);
+
+ String value = config.getProperty(TESTDATA_KEY);
+
+ assertEquals(TESTDATA_VAL, value);
+ }
+
+ @Test
+ void testGetProperty_ConfigNotLoaded() {
+ NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> {
+ config.getProperty(TESTDATA_KEY);
+ });
+
+ assertEquals("Config has not been loaded", exception.getMessage());
+ }
+
+ @Test
+ void testGetProperty_NonExistingKey() {
+ try {
+ config.setConfigLocation(TESTDATA_PATH);
+ config.loadConfig();
+
+ NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> {
+ config.getProperty("I_DONT_EXIST");
+ });
+
+ assertEquals("Requested config value does not exist", exception.getMessage());
+ } catch (ConfigurationException e) {
+ fail("ConfigurationException should not be thrown");
+ }
+ }
+
+ @Test
+ void testSetProperty() {
+ try {
+ config.setConfigLocation(TESTDATA_PATH);
+ config.loadConfig();
+ config.setProperty(TESTDATA_KEY, "12345");
+
+ assertEquals("12345", config.getProperty(TESTDATA_KEY));
+ } catch (ConfigurationException e) {
+ fail("ConfigurationException should not be thrown");
+ }
+ }
+
+ @Test
+ void testLoadConfigSuccessfully() {
+ try {
+ config.setConfigLocation("testResources/testdata.properties");
+ config.loadConfig();
+ assertEquals(TESTDATA_VAL, config.getProperty(TESTDATA_KEY));
+ } catch (ConfigurationException e) {
+ fail("ConfigurationException should not be thrown");
+ }
+ }
+
+ @Test
+ void testLoadConfigMissingFile() {
+ config.setConfigLocation("path/that/doesnt/exist/config.properties");
+
+ ConfigurationException exception = assertThrows(ConfigurationException.class, () -> {
+ config.loadConfig();
+ });
+
+ assertEquals("Couldn't load required config file", exception.getMessage());
+ }
+}
diff --git a/src/test/java/de/rwu/easydrop/util/ConfigTest.java b/src/test/java/de/rwu/easydrop/util/ConfigTest.java
new file mode 100644
index 0000000..8cf0c2a
--- /dev/null
+++ b/src/test/java/de/rwu/easydrop/util/ConfigTest.java
@@ -0,0 +1,42 @@
+package de.rwu.easydrop.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.spy;
+
+import javax.naming.ConfigurationException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockitoAnnotations;
+
+class ConfigTest {
+ private Config config;
+
+ @BeforeEach
+ void setUp() throws ConfigurationException {
+ MockitoAnnotations.openMocks(this);
+ config = spy(Config.getInstance());
+ }
+
+ @Test
+ void testGetInstanceNull() {
+ config = null;
+
+ Config newConfig = Config.getInstance();
+ assertNotNull(newConfig);
+ }
+
+ @Test
+ void testGetInstanceNotNull() {
+ Config newConfig = Config.getInstance();
+ assertNotNull(newConfig);
+ }
+
+ @Test
+ void testSetConfigLocation() {
+ String newPath = "new/location/config.properties";
+ config.setConfigLocation(newPath);
+ assertEquals(newPath, config.getConfigLocation());
+ }
+}
diff --git a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java
new file mode 100644
index 0000000..1c7d4f9
--- /dev/null
+++ b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java
@@ -0,0 +1,58 @@
+package de.rwu.easydrop.util;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class FormattingUtilTest {
+
+ @Test
+ void testConstructorIsPrivate()
+ throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ // Check for private constructor
+ Constructor constructor = FormattingUtil.class.getDeclaredConstructor();
+ assertTrue(Modifier.isPrivate(constructor.getModifiers()));
+
+ // Make sure exception is thrown when instantiating
+ constructor.setAccessible(true);
+ assertThrows(InvocationTargetException.class, () -> {
+ constructor.newInstance();
+ });
+ }
+
+ @Test
+ void testFormatEuro_positiveAmount() {
+ double amount = 1234.56;
+ String expectedFormattedAmount = "1.234,56 €";
+
+ String formattedAmount = FormattingUtil.formatEuro(amount);
+
+ Assertions.assertEquals(expectedFormattedAmount, formattedAmount);
+ }
+
+ @Test
+ void testFormatEuro_zeroAmount() {
+ double amount = 0.0;
+ String expectedFormattedAmount = "0,00 €";
+
+ String formattedAmount = FormattingUtil.formatEuro(amount);
+
+ Assertions.assertEquals(expectedFormattedAmount, formattedAmount);
+ }
+
+ @Test
+ void testFormatEuro_negativeAmount() {
+ double amount = -789.12;
+ String expectedFormattedAmount = "-789,12 €";
+
+ String formattedAmount = FormattingUtil.formatEuro(amount);
+
+ Assertions.assertEquals(expectedFormattedAmount, formattedAmount);
+ }
+}
diff --git a/testResources/empty.properties b/testResources/empty.properties
new file mode 100644
index 0000000..e69de29
diff --git a/testResources/testdata.properties b/testResources/testdata.properties
new file mode 100644
index 0000000..cbf5875
--- /dev/null
+++ b/testResources/testdata.properties
@@ -0,0 +1 @@
+API_KEY=keyIsHere
\ No newline at end of file