From 931d5eef576475d5ef94b19e3879910c05cf25dd Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Wed, 14 Jun 2023 14:38:31 +0200 Subject: [PATCH 01/71] Grobkonstrukt Klasse Preisentnahme der AmazonAPI --- .../de/rwu/easydrop/api/Preisentnahme.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/de/rwu/easydrop/api/Preisentnahme.java diff --git a/src/main/java/de/rwu/easydrop/api/Preisentnahme.java b/src/main/java/de/rwu/easydrop/api/Preisentnahme.java new file mode 100644 index 0000000..073ef9b --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/Preisentnahme.java @@ -0,0 +1,49 @@ +package de.rwu.easydrop.api.client; + +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; + +/** + * Diese Klasse AmazonPriceFetcher enthält die Methode getProductPrice, + * die den Preis eines Produkts von der Amazon API abruft. + * Die Methode verwendet die Base URL, den API-Schlüssel und die Produkt-ID, + * um die API-URL zu erstellen und die Daten von der API abzurufen. + * Anschließend wird der Preis aus der JSON-Antwort extrahiert und zurückgegeben. + */ +public final class AmazonPriceFetcher { + /** + * URL zur Amazon API. + */ + private String baseUrl; + /** + * API-Schlüssel zur Autorisierung. + */ + private String apiKey; + /** + * Produktregion für den Datenzugriff. + */ + private static final String PRODUCT_REGION = "DE"; + /** + * Sprachparameter für den Datenzugriff. + */ + private static final String LOCALE = "de_DE"; + + /** + * Initialisiert die Klasse mit der Base URL und dem API-Schlüssel. + * + * @param newBaseUrl Die Base URL zur Amazon API. + * @param newApiKey Der API-Schlüssel zur Autorisierung. + */ + + public AmazonPriceFetcher(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + +} \ No newline at end of file From 36de6ce7bb34a144c8a4916c1d27e194be83032c Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Wed, 14 Jun 2023 14:47:33 +0200 Subject: [PATCH 02/71] Klasse Preisentnahme AmazonAPI Konstrukt --- src/main/java/de/rwu/easydrop/api/Preisentnahme.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/de/rwu/easydrop/api/Preisentnahme.java b/src/main/java/de/rwu/easydrop/api/Preisentnahme.java index 073ef9b..8041bb5 100644 --- a/src/main/java/de/rwu/easydrop/api/Preisentnahme.java +++ b/src/main/java/de/rwu/easydrop/api/Preisentnahme.java @@ -46,4 +46,6 @@ public final class AmazonPriceFetcher { this.apiKey = newApiKey; } + + } \ No newline at end of file From 82a0569e9db84822ed202051275888343a663acb Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 18 Jun 2023 18:26:17 +0200 Subject: [PATCH 03/71] Abrufen der PRodukte der AmazonAPI --- src/main/java/de/rwu/easydrop/Main.java | 12 ++++ .../java/de/rwu/easydrop/PriceFetcher.java | 61 +++++++++++++++++++ .../java/de/rwu/easydrop/api/BeforeEach.java | 5 ++ .../de/rwu/easydrop/api/Preisentnahme.java | 51 ---------------- src/main/java/de/rwu/easydrop/api/Test.java | 5 ++ .../rwu/easydrop/model/ProductCatalogue.java | 1 + .../easydrop/api/AmazonPriceFetcherTest.java | 56 +++++++++++++++++ 7 files changed, 140 insertions(+), 51 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/PriceFetcher.java create mode 100644 src/main/java/de/rwu/easydrop/api/BeforeEach.java delete mode 100644 src/main/java/de/rwu/easydrop/api/Preisentnahme.java create mode 100644 src/main/java/de/rwu/easydrop/api/Test.java create mode 100644 src/test/java/de/rwu/easydrop/api/AmazonPriceFetcherTest.java diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index 5441d96..efc9a60 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -1,5 +1,6 @@ package de.rwu.easydrop; +import java.net.MalformedURLException; import java.util.List; import javax.naming.ConfigurationException; @@ -11,6 +12,7 @@ import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.data.connector.SQLiteConnector; +import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.service.retriever.CatalogueRetriever; import de.rwu.easydrop.service.retriever.ProductRetriever; @@ -58,5 +60,15 @@ public final class Main { String pCatStr = pCat.toString(); LOGGER.info(pCatStr); } + + for (ProductCatalogue pCat : pCats) { + List products = pCat.getProducts(); + + for (Product product : products) { + String productId = product.getProductId(); + double price = product.getCurrentPrice(); + LOGGER.info("Product ID: " + productId + ", Price: " + price); + } + } } } diff --git a/src/main/java/de/rwu/easydrop/PriceFetcher.java b/src/main/java/de/rwu/easydrop/PriceFetcher.java new file mode 100644 index 0000000..f25a371 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/PriceFetcher.java @@ -0,0 +1,61 @@ +package de.rwu.easydrop; + +import java.net.MalformedURLException; +import java.util.List; + +import javax.naming.ConfigurationException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sqlite.SQLiteDataSource; + +import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.SQLiteConnector; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.retriever.CatalogueRetriever; +import de.rwu.easydrop.service.retriever.ProductRetriever; +import de.rwu.easydrop.service.writer.CatalogueWriter; +import de.rwu.easydrop.util.Config; +import de.rwu.easydrop.util.ProductsConfig; + + +/** + * Diese Klasse PriceFetcher enthält die Methode getProductPrice, + * die den Preis eines Produkts von den jeweiligen API's abruft. + */ +public final class PriceFetcher { + + private static final Logger LOGGER = LoggerFactory.getLogger(PriceFetcher.class); + + public static void getProductPrice(final String[] args) throws ConfigurationException{ + + Config config = Config.getInstance(); + ProductsConfig pConfig = ProductsConfig.getInstance(); + DataSourceFactory dataSourceFactory = new DataSourceFactory(config); + ProductRetriever retriever = new ProductRetriever(dataSourceFactory); + CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); + AbstractProductPersistence db = new SQLiteConnector(new SQLiteDataSource()); + CatalogueWriter catWriter = new CatalogueWriter(db); + + catRetriever.loadCatalogues(); + List pCats = catRetriever.getProductCatalogues(); + catWriter.writeCatalogues(pCats); + + for (ProductCatalogue pCat : pCats) { + String pCatStr = pCat.toString(); + LOGGER.info(pCatStr); + } + + for (ProductCatalogue pCat : pCats) { + List products = pCat.getProducts(); + + for (Product product : products) { + String productId = product.getProductId(); + double price = product.getCurrentPrice(); + LOGGER.info("Product ID: " + productId + ", Price: " + price); + } + } + } +} diff --git a/src/main/java/de/rwu/easydrop/api/BeforeEach.java b/src/main/java/de/rwu/easydrop/api/BeforeEach.java new file mode 100644 index 0000000..b316851 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/BeforeEach.java @@ -0,0 +1,5 @@ +package de.rwu.easydrop.api; + +public @interface BeforeEach { + +} diff --git a/src/main/java/de/rwu/easydrop/api/Preisentnahme.java b/src/main/java/de/rwu/easydrop/api/Preisentnahme.java deleted file mode 100644 index 8041bb5..0000000 --- a/src/main/java/de/rwu/easydrop/api/Preisentnahme.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.rwu.easydrop.api.client; - -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; - -/** - * Diese Klasse AmazonPriceFetcher enthält die Methode getProductPrice, - * die den Preis eines Produkts von der Amazon API abruft. - * Die Methode verwendet die Base URL, den API-Schlüssel und die Produkt-ID, - * um die API-URL zu erstellen und die Daten von der API abzurufen. - * Anschließend wird der Preis aus der JSON-Antwort extrahiert und zurückgegeben. - */ -public final class AmazonPriceFetcher { - /** - * URL zur Amazon API. - */ - private String baseUrl; - /** - * API-Schlüssel zur Autorisierung. - */ - private String apiKey; - /** - * Produktregion für den Datenzugriff. - */ - private static final String PRODUCT_REGION = "DE"; - /** - * Sprachparameter für den Datenzugriff. - */ - private static final String LOCALE = "de_DE"; - - /** - * Initialisiert die Klasse mit der Base URL und dem API-Schlüssel. - * - * @param newBaseUrl Die Base URL zur Amazon API. - * @param newApiKey Der API-Schlüssel zur Autorisierung. - */ - - public AmazonPriceFetcher(final String newBaseUrl, final String newApiKey) { - this.baseUrl = newBaseUrl; - this.apiKey = newApiKey; - } - - - -} \ No newline at end of file diff --git a/src/main/java/de/rwu/easydrop/api/Test.java b/src/main/java/de/rwu/easydrop/api/Test.java new file mode 100644 index 0000000..ca98b8c --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/Test.java @@ -0,0 +1,5 @@ +package de.rwu.easydrop.api; + +public @interface Test { + +} diff --git a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java index 97f31dc..7f0ad97 100644 --- a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java +++ b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java @@ -24,6 +24,7 @@ public class ProductCatalogue { * Product collection. */ private List products; + public float getProducts; /** * Creates new Product Catalogue. diff --git a/src/test/java/de/rwu/easydrop/api/AmazonPriceFetcherTest.java b/src/test/java/de/rwu/easydrop/api/AmazonPriceFetcherTest.java new file mode 100644 index 0000000..4be1200 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/AmazonPriceFetcherTest.java @@ -0,0 +1,56 @@ +package de.rwu.easydrop.api; + +import java.net.MalformedURLException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AmazonPriceFetcherTest { + private static final String BASE_URL = "https://api.amazon.com"; + private static final String API_KEY = "your_api_key"; + private static final String PRODUCT_ID = "12345"; + + private AmazonPriceFetcher priceFetcher; + + @BeforeEach + public void setup() { + priceFetcher = new AmazonPriceFetcher(BASE_URL, API_KEY); + } + + @Test + public void testGetProductPrice_ValidProductId_ReturnsPrice() throws MalformedURLException { + // Arrange + double expectedPrice = -1.0; + + // Act + double actualPrice = priceFetcher.getProductPrice(PRODUCT_ID); + + // Assert + Assertions.assertEquals(expectedPrice, actualPrice); + } + + @Test + public void testGetProductPrice_InvalidProductId_ReturnsNegativePrice() throws MalformedURLException { + // Arrange + double expectedPrice = -1.0; + + // Act + double actualPrice = priceFetcher.getProductPrice("invalid_product_id"); + + // Assert + Assertions.assertEquals(expectedPrice, actualPrice); + } + + @Test + public void testGetProductPrice_ExceptionThrown_ReturnsNegativePrice() throws MalformedURLException { + // Arrange + double expectedPrice = -1.0; + + // Act + double actualPrice = priceFetcher.getProductPrice("exception_thrown"); + + // Assert + Assertions.assertEquals(expectedPrice, actualPrice); + } +} \ No newline at end of file From 0be5a5ad400d7b2e3d7169f84403e23972e3384a Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Thu, 22 Jun 2023 16:31:54 +0000 Subject: [PATCH 04/71] Amazon and eBay Buy/Sell APIs --- .vscode/settings.json | 3 +- CHANGELOG.md | 11 +- config/custom-checkstyle.xml | 5 + config/suppressions.xml | 6 + .../api/client/AbstractDataSource.java | 2 +- .../api/client/AbstractDataWriter.java | 81 ++++++++++++ .../api/client/AbstractPurchaser.java | 19 +++ .../easydrop/api/client/AbstractSeller.java | 19 +++ .../easydrop/api/client/AmazonPurchaser.java | 67 ++++++++++ .../rwu/easydrop/api/client/AmazonSeller.java | 67 ++++++++++ .../easydrop/api/client/EbayPurchaser.java | 52 ++++++++ .../rwu/easydrop/api/client/EbaySeller.java | 52 ++++++++ .../exception/DataWriterException.java | 27 ++++ .../api/client/AbstractDataWriterTest.java | 121 ++++++++++++++++++ .../api/client/AbstractPurchaserTest.java | 38 ++++++ .../api/client/AbstractSellerTest.java | 38 ++++++ .../api/client/AmazonPurchaserTest.java | 84 ++++++++++++ .../easydrop/api/client/AmazonSellerTest.java | 80 ++++++++++++ .../api/client/EbayPurchaserTest.java | 77 +++++++++++ .../easydrop/api/client/EbaySellerTest.java | 77 +++++++++++ .../exception/DataWriterExceptionTest.java | 61 +++++++++ 21 files changed, 980 insertions(+), 7 deletions(-) create mode 100644 config/suppressions.xml create mode 100644 src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java create mode 100644 src/main/java/de/rwu/easydrop/api/client/AbstractPurchaser.java create mode 100644 src/main/java/de/rwu/easydrop/api/client/AbstractSeller.java create mode 100644 src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java create mode 100644 src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java create mode 100644 src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java create mode 100644 src/main/java/de/rwu/easydrop/api/client/EbaySeller.java create mode 100644 src/main/java/de/rwu/easydrop/exception/DataWriterException.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java create mode 100644 src/test/java/de/rwu/easydrop/exception/DataWriterExceptionTest.java diff --git a/.vscode/settings.json b/.vscode/settings.json index b72b3e1..d861443 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "connectionId": "LocalSonarQube", "projectKey": "EasyDrop" }, - "java.debug.settings.onBuildFailureProceed": true + "java.debug.settings.onBuildFailureProceed": true, + "java.checkstyle.configuration": "${workspaceFolder}\\config\\custom-checkstyle.xml" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 5498b1c..a8f50bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,12 @@ ### Added -- Added script for local UML generation (#56) - -### Removed - -- Document stage from CI run (#56) +- Script for local UML generation (#56) +- Transaction fulfillment API classes (#71) + - `AmazonSeller` + - `AmazonPurchaser` + - `EbaySeller` + - `EbayPurchaser` ## 0.2.0 diff --git a/config/custom-checkstyle.xml b/config/custom-checkstyle.xml index 71d9e97..21428fc 100644 --- a/config/custom-checkstyle.xml +++ b/config/custom-checkstyle.xml @@ -44,6 +44,11 @@ https://www.oracle.com/java/technologies/javase/codeconventions-contents.html + + + + + diff --git a/config/suppressions.xml b/config/suppressions.xml new file mode 100644 index 0000000..4067d19 --- /dev/null +++ b/config/suppressions.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java index 7751c10..4b58d66 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java @@ -42,7 +42,7 @@ public abstract class AbstractDataSource implements DataSource { protected abstract ProductDTO buildProductDTO(ProductDTO product, String json); /** - * Overridable standard implementation. + * Pulls product data from a remote data source. */ @Override public ProductDTO getProductDTOById(final String productIdentifier) diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java b/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java new file mode 100644 index 0000000..fd7a615 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java @@ -0,0 +1,81 @@ +package de.rwu.easydrop.api.client; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.DataWriterException; +import de.rwu.easydrop.util.FormattingUtil; + +/** + * Writes data to a remote API. + * + * @since 0.3.0 + */ +public abstract class AbstractDataWriter { + + /** + * Returns the data source's API key. + * + * @return Data source API key + */ + protected abstract String getApiKey(); + + /** + * Returns the target API's name. + * + * @return Data target API name + */ + protected abstract String getDataTarget(); + + /** + * Creates an URL object to connect to the API with. + * + * @param productIdentifier Product identifier + * @return URL object + * @throws MalformedURLException + */ + protected abstract URL createApiUrl(String productIdentifier) throws MalformedURLException; + + /** + * Sends a put request to the API. + * + * @param dto Product data transfer object + * @param apiType API Type, like Purchase or Sales + */ + protected void sendPutRequest(final ProductDTO dto, final String apiType) { + String apiKey = getApiKey(); + String dataTarget = getDataTarget(); + + if (!dataTarget.equals(dto.getDataOrigin())) { + throw new DataWriterException( + "Product data source and target " + apiType + " API are incompatible."); + } + + try { + String urlReadyIdentifier = FormattingUtil.urlEncode(dto.getProductId()); + URL apiUrl = createApiUrl(urlReadyIdentifier); + + HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod("PUT"); + connection.setRequestProperty("Credential", apiKey); + + int responseCode = connection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new DataWriterException( + dataTarget + + " " + + apiType + + " API responded with error code " + + responseCode); + } + } catch (IOException e) { + throw new DataWriterException( + "Couldn't fulfill " + + dataTarget + + " API request"); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/AbstractPurchaser.java new file mode 100644 index 0000000..67700a0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractPurchaser.java @@ -0,0 +1,19 @@ +package de.rwu.easydrop.api.client; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Abstract construct to provide access to a purchase API. + * + * @since 0.3.0 + */ +public abstract class AbstractPurchaser extends AbstractDataWriter { + /** + * Sends a buy request to the Amazon API. + * + * @param dto + */ + public void purchaseProduct(final ProductDTO dto) { + sendPutRequest(dto, "Purchase"); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractSeller.java b/src/main/java/de/rwu/easydrop/api/client/AbstractSeller.java new file mode 100644 index 0000000..8d659d7 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractSeller.java @@ -0,0 +1,19 @@ +package de.rwu.easydrop.api.client; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Abstract construct to provide access to a sales API. + * + * @since 0.3.0 + */ +public abstract class AbstractSeller extends AbstractDataWriter { + /** + * Sends a sell request to the Amazon API. + * + * @param dto + */ + public void sellProduct(final ProductDTO dto) { + sendPutRequest(dto, "Sales"); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java new file mode 100644 index 0000000..283ed61 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java @@ -0,0 +1,67 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Sends a buy request to the Amazon API. + * + * @since 0.3.0 + */ +public final class AmazonPurchaser extends AbstractPurchaser { + /** + * Name of this data source. + */ + private static final String DATA_TARGET = "Amazon"; + /** + * Base URL to the Amazon Purchase API. + */ + private String baseUrl; + /** + * Access credential. + */ + private String apiKey; + /** + * Product region parameter required for data writes. + */ + private static final String PRODUCT_REGION = "DE"; + /** + * Locale parameter required for data writes. + */ + private static final String LOCALE = "de_DE"; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public AmazonPurchaser(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + /** + * @param productId ASIN + */ + @Override + protected URL createApiUrl(final String productId) throws MalformedURLException { + return new URL(baseUrl + + "/products/2020-08-26/products/" + + productId + + "/buy?productRegion=" + + PRODUCT_REGION + + "&locale=" + + LOCALE); + } + + @Override + protected String getApiKey() { + return this.apiKey; + } + + @Override + protected String getDataTarget() { + return DATA_TARGET; + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java b/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java new file mode 100644 index 0000000..8a47d51 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java @@ -0,0 +1,67 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Sends a sell request to the Amazon API. + * + * @since 0.3.0 + */ +public final class AmazonSeller extends AbstractSeller { + /** + * Name of this data source. + */ + private static final String DATA_TARGET = "Amazon"; + /** + * Base URL to the Amazon Purchase API. + */ + private String baseUrl; + /** + * Access credential. + */ + private String apiKey; + /** + * Product region parameter required for data writes. + */ + private static final String PRODUCT_REGION = "DE"; + /** + * Locale parameter required for data writes. + */ + private static final String LOCALE = "de_DE"; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public AmazonSeller(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + /** + * @param productId ASIN + */ + @Override + protected URL createApiUrl(final String productId) throws MalformedURLException { + return new URL(baseUrl + + "/products/2020-08-26/products/" + + productId + + "/sell?productRegion=" + + PRODUCT_REGION + + "&locale=" + + LOCALE); + } + + @Override + protected String getApiKey() { + return apiKey; + } + + @Override + protected String getDataTarget() { + return DATA_TARGET; + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java new file mode 100644 index 0000000..d9f69f8 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java @@ -0,0 +1,52 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Sends a buy request to the eBay API. + * + * @since 0.3.0 + */ +public final class EbayPurchaser extends AbstractPurchaser { + /** + * Name of this data source. + */ + private static final String DATA_TARGET = "eBay"; + /** + * Base URL to the eBay Purchase API. + */ + private String baseUrl; + /** + * Access credential. + */ + private String apiKey; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public EbayPurchaser(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + @Override + protected URL createApiUrl(final String searchQuery) throws MalformedURLException { + return new URL(baseUrl + + "/buy/" + + searchQuery); + } + + @Override + protected String getApiKey() { + return apiKey; + } + + @Override + protected String getDataTarget() { + return DATA_TARGET; + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java b/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java new file mode 100644 index 0000000..805ee5d --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java @@ -0,0 +1,52 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Sends a sell request to the eBay API. + * + * @since 0.3.0 + */ +public final class EbaySeller extends AbstractSeller { + /** + * Name of this data source. + */ + private static final String DATA_TARGET = "eBay"; + /** + * Base URL to the eBay Purchase API. + */ + private String baseUrl; + /** + * Access credential. + */ + private String apiKey; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public EbaySeller(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + @Override + protected URL createApiUrl(final String searchQuery) throws MalformedURLException { + return new URL(baseUrl + + "/sell/" + + searchQuery); + } + + @Override + protected String getApiKey() { + return this.apiKey; + } + + @Override + protected String getDataTarget() { + return DATA_TARGET; + } +} diff --git a/src/main/java/de/rwu/easydrop/exception/DataWriterException.java b/src/main/java/de/rwu/easydrop/exception/DataWriterException.java new file mode 100644 index 0000000..6c1757e --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/DataWriterException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies a communication problem with a writer API. + * + * @since 0.3.0 + */ +public class DataWriterException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public DataWriterException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public DataWriterException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java new file mode 100644 index 0000000..edc8ad8 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java @@ -0,0 +1,121 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.DataWriterException; + +class AbstractDataWriterTest { + private static String demoProductId = "whateverId"; + + @Mock + private HttpURLConnection mockConnection; + @Mock + private URL mockUrl; + + private AbstractDataWriter writer; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + writer = new AbstractDataWriter() { + @Override + protected String getApiKey() { + return "testApiKey"; + } + + @Override + protected String getDataTarget() { + return "Test"; + } + + @Override + protected URL createApiUrl(String productIdentifier) throws MalformedURLException { + return mockUrl; + } + }; + } + + @Test + void sendPutRequest_wrongDataSource_throwsException() { + ProductDTO dto = new ProductDTO(demoProductId, "Amazon"); + + DataWriterException e = assertThrows(DataWriterException.class, () -> { + writer.sendPutRequest(dto, "testApiType"); + }); + + assertEquals("Product data source and target testApiType API are incompatible.", e.getMessage()); + } + + @Test + void sendPutRequest_badResponseCode_throwsException() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO(demoProductId, "Test"); + + // Set up Mocks + AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); + doCallRealMethod().when(mockWriter).sendPutRequest(any(), anyString()); + when(mockUrl.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST); + + DataWriterException e = assertThrows(DataWriterException.class, () -> { + writer.sendPutRequest(dto, "Sales"); + }); + + assertEquals("Test Sales API responded with error code 400", e.getMessage()); + } + + @Test + void sendPutRequest_ioException_throwsException() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO(demoProductId, "Test"); + + // Set up Mocks + AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); + doCallRealMethod().when(mockWriter).sendPutRequest(any(), anyString()); + when(mockUrl.openConnection()).thenThrow(new IOException()); + + DataWriterException e = assertThrows(DataWriterException.class, () -> { + writer.sendPutRequest(dto, "testApiType"); + }); + + assertEquals("Couldn't fulfill Test API request", e.getMessage()); + } + + @Test + void sendPutRequest_successfulRequest() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO(demoProductId, "test"); + + // Set up Mocks + AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); + URL mockURL = mock(URL.class); + when(mockWriter.createApiUrl(demoProductId)).thenReturn(mockURL); + doCallRealMethod().when(mockWriter).sendPutRequest(any(), anyString()); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockWriter.getDataTarget()).thenReturn("test"); + + assertDoesNotThrow(() -> { + mockWriter.sendPutRequest(dto, "Purchase"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java new file mode 100644 index 0000000..f252264 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java @@ -0,0 +1,38 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.api.dto.ProductDTO; + +class AbstractPurchaserTest { + private AbstractPurchaser mockPurchaser = mock(AbstractPurchaser.class); + + @Test + void purchaseProduct_CorrectApiTypeInException() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO("12345", "test"); + + // Set up mocks + URL mockURL = mock(URL.class); + when(mockPurchaser.createApiUrl("12345")).thenReturn(mockURL); + doCallRealMethod().when(mockPurchaser).purchaseProduct(any()); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockPurchaser.getDataTarget()).thenReturn("test"); + + assertDoesNotThrow(() -> { + mockPurchaser.purchaseProduct(dto); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java new file mode 100644 index 0000000..eb7e595 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java @@ -0,0 +1,38 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.api.dto.ProductDTO; + +class AbstractSellerTest { + private AbstractSeller mockSeller = mock(AbstractSeller.class); + + @Test + void purchaseProduct_CorrectApiTypeInException() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO("12345", "test"); + + // Set up mocks + URL mockURL = mock(URL.class); + when(mockSeller.createApiUrl("12345")).thenReturn(mockURL); + doCallRealMethod().when(mockSeller).sellProduct(any()); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockSeller.getDataTarget()).thenReturn("test"); + + assertDoesNotThrow(() -> { + mockSeller.sellProduct(dto); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java new file mode 100644 index 0000000..ec1e1d6 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java @@ -0,0 +1,84 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.lang.reflect.Field; +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; +import de.rwu.easydrop.exception.DataWriterException; + +class AmazonPurchaserTest { + private AmazonPurchaser demoPurchaser; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static String demoDataTarget = "Amazon"; + + @BeforeEach + void setup() { + demoPurchaser = new AmazonPurchaser( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = AmazonPurchaser.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoPurchaser)); + + Field apiKeyField = AmazonPurchaser.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoPurchaser)); + } 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/" + + productId1 + + "/buy?productRegion=DE&locale=de_DE"); + URL createdUrl1 = demoPurchaser.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/" + + productId2 + + "/buy?productRegion=DE&locale=de_DE"); + URL createdUrl2 = demoPurchaser.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void getDataTarget_ReturnsExpectedDataOrigin() { + String dataTarget = demoPurchaser.getDataTarget(); + assertEquals(demoDataTarget, dataTarget); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoPurchaser.getApiKey(); + assertEquals(demoApiKey, apiKey); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java new file mode 100644 index 0000000..06176cc --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java @@ -0,0 +1,80 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +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; + +class AmazonSellerTest { + private AmazonSeller demoSeller; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static String demoDataTarget = "Amazon"; + + @BeforeEach + void setup() { + demoSeller = new AmazonSeller( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = AmazonSeller.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoSeller)); + + Field apiKeyField = AmazonSeller.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoSeller)); + } 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/" + + productId1 + + "/sell?productRegion=DE&locale=de_DE"); + URL createdUrl1 = demoSeller.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/" + + productId2 + + "/sell?productRegion=DE&locale=de_DE"); + URL createdUrl2 = demoSeller.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void getDataTarget_ReturnsExpectedDataOrigin() { + String dataTarget = demoSeller.getDataTarget(); + assertEquals(demoDataTarget, dataTarget); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoSeller.getApiKey(); + assertEquals(demoApiKey, apiKey); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java new file mode 100644 index 0000000..238e2c5 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java @@ -0,0 +1,77 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +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; + +class EbayPurchaserTest { + private EbayPurchaser demoPurchaser; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static String demoDataTarget = "eBay"; + + @BeforeEach + void setup() { + demoPurchaser = new EbayPurchaser( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = EbayPurchaser.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoPurchaser)); + + Field apiKeyField = EbayPurchaser.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoPurchaser)); + } 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/buy/" + + productId1); + URL createdUrl1 = demoPurchaser.createApiUrl(productId1); + Assertions.assertEquals(expectedUrl1, createdUrl1); + + // Test case 2 + String productId2 = "67890"; + URL expectedUrl2 = new URL( + "https://www.example.com/api/buy/" + + productId2); + URL createdUrl2 = demoPurchaser.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void getDataTarget_ReturnsExpectedDataOrigin() { + String dataTarget = demoPurchaser.getDataTarget(); + assertEquals(demoDataTarget, dataTarget); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoPurchaser.getApiKey(); + assertEquals(demoApiKey, apiKey); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java b/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java new file mode 100644 index 0000000..978aa63 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java @@ -0,0 +1,77 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +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; + +class EbaySellerTest { + private EbaySeller demoSeller; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static String demoDataTarget = "eBay"; + + @BeforeEach + void setup() { + demoSeller = new EbaySeller( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = EbaySeller.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoSeller)); + + Field apiKeyField = EbaySeller.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoSeller)); + } 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/sell/" + + productId1); + URL createdUrl1 = demoSeller.createApiUrl(productId1); + Assertions.assertEquals(expectedUrl1, createdUrl1); + + // Test case 2 + String productId2 = "67890"; + URL expectedUrl2 = new URL( + "https://www.example.com/api/sell/" + + productId2); + URL createdUrl2 = demoSeller.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void getDataTarget_ReturnsExpectedDataOrigin() { + String dataTarget = demoSeller.getDataTarget(); + assertEquals(demoDataTarget, dataTarget); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoSeller.getApiKey(); + assertEquals(demoApiKey, apiKey); + } +} diff --git a/src/test/java/de/rwu/easydrop/exception/DataWriterExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/DataWriterExceptionTest.java new file mode 100644 index 0000000..fd00b3e --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/DataWriterExceptionTest.java @@ -0,0 +1,61 @@ +package de.rwu.easydrop.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class DataWriterExceptionTest { + + @Test + void constructor_WithMessage_SetsMessage() { + // Arrange + String message = "Data source error"; + + // Act + DataWriterException exception = new DataWriterException(message); + + // Assert + assertEquals(message, exception.getMessage()); + } + + @Test + void constructor_WithMessageAndCause_SetsMessageAndCause() { + // Arrange + String message = "Data source error"; + Throwable cause = new IllegalArgumentException("Invalid argument"); + + // Act + DataWriterException exception = new DataWriterException(message, cause); + + // Assert + assertEquals(message, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } + + @Test + void constructor_WithNullMessage_SetsNullMessage() { + // Act + DataWriterException exception = new DataWriterException(null); + + // Assert + assertEquals(null, exception.getMessage()); + } + + @Test + void constructor_WithNullCause_SetsNullCause() { + // Act + DataWriterException exception = new DataWriterException("Data source error", null); + + // Assert + assertEquals(null, exception.getCause()); + } + + @Test + void throw_DataWriterException() { + // Act and Assert + assertThrows(DataWriterException.class, () -> { + throw new DataWriterException("Data source error"); + }); + } +} From dce001f1b3cfbf5f644e9252798dc9aa01c5018c Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Fri, 23 Jun 2023 12:40:36 +0200 Subject: [PATCH 05/71] Funktion zur Margenermittlung --- .../java/de/rwu/easydrop/PriceFetcher.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/main/java/de/rwu/easydrop/PriceFetcher.java b/src/main/java/de/rwu/easydrop/PriceFetcher.java index f25a371..4ccf2e5 100644 --- a/src/main/java/de/rwu/easydrop/PriceFetcher.java +++ b/src/main/java/de/rwu/easydrop/PriceFetcher.java @@ -1,6 +1,7 @@ package de.rwu.easydrop; import java.net.MalformedURLException; +import java.util.ArrayList; import java.util.List; import javax.naming.ConfigurationException; @@ -26,9 +27,36 @@ import de.rwu.easydrop.util.ProductsConfig; * die den Preis eines Produkts von den jeweiligen API's abruft. */ public final class PriceFetcher { + static List api1Prices = new ArrayList<>(); + static List api2Prices = new ArrayList<>(); private static final Logger LOGGER = LoggerFactory.getLogger(PriceFetcher.class); + //Methode, um Preise der Prdukte zu vergleichen -> MArgen ermitteln + public static void comparePrices() throws ConfigurationException { + List api1Prices = new ArrayList<>(); + List api2Prices = new ArrayList<>(); + + for (int i = 0; i < api1Prices.size(); i++) { + double api1Price = api1Prices.get(i); + double api2Price = api2Prices.get(i); + String productId = "Product " + (i + 1); + + if (api1Price < api2Price) { + double priceDifference = api2Price - api1Price; + LOGGER.info("API 1 hat einen günstigeren Preis für " + productId + + ". Der Preisunterschied beträgt: " + priceDifference); + } else if (api2Price < api1Price) { + double priceDifference = api1Price - api2Price; + LOGGER.info("API 2 hat einen günstigeren Preis für " + productId + + ". Der Preisunterschied beträgt: " + priceDifference); + } else { + LOGGER.info("Beide APIs haben den gleichen Preis für " + productId); + } + } +} + + // Methode, um Preise der Produkte zu ermitteln public static void getProductPrice(final String[] args) throws ConfigurationException{ Config config = Config.getInstance(); @@ -55,7 +83,14 @@ public final class PriceFetcher { String productId = product.getProductId(); double price = product.getCurrentPrice(); LOGGER.info("Product ID: " + productId + ", Price: " + price); + if (productId == "B096Y2TYKV") { + api1Prices.add(price); + } else if (productId == "Gigabyte GeForce RTX 3060") { + api2Prices.add(price); + } } } } + + } From 3a1450dade275cdbbdd94cf69e4b6cbe2873c88c Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Sat, 24 Jun 2023 23:34:13 +0000 Subject: [PATCH 06/71] Docker/-compose --- CHANGELOG.md | 1 + Dockerfile | 28 ++++++++++++++++++++++++++++ README.md | 33 ++++++++++++++++++++------------- Script/Bump_Version.sh | 23 +++++++++++++++++++++++ Script/SonarQube_Local.sh | 5 ++--- docker-compose.yml | 11 +++++++++++ pom.xml | 21 +++++++++++++++++++++ 7 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 Dockerfile create mode 100644 Script/Bump_Version.sh create mode 100644 docker-compose.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f50bb..9ef2078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `AmazonPurchaser` - `EbaySeller` - `EbayPurchaser` +- Dockerfile and `docker-compose.yml` ## 0.2.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3939c20 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Use the official maven/Java 17 image to create a build artifact. +# https://hub.docker.com/_/maven +FROM maven:3.9.1-eclipse-temurin-17 AS build + +# Set the working directory in the image to /app +WORKDIR /app + +# Copy the pom.xml file into the current directory (/app) in the image +COPY pom.xml . + +# Download all required dependencies into one layer +RUN mvn -B dependency:go-offline + +# Copy the rest of the application source code +COPY src /app/src + +# Build the application +RUN mvn -B package -DskipTests + +# Use OpenJDK 17 for the runtime stage of the Dockerfile +FROM openjdk:17-jdk-slim + +# Copy the jar file from the build stage +COPY --from=build /app/target/easydrop-0.3.0-SNAPSHOT.jar /easydrop.jar + +# Execute the application when the docker container starts. +ENTRYPOINT ["java", "-jar", "/easydrop.jar"] + diff --git a/README.md b/README.md index e465e36..e4827d7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # EasyDrop -![Version](https://img.shields.io/badge/version-0.3.0_SNAPSHOT-orange) +![Version](https://img.shields.io/badge/version-0.3.0–SNAPSHOT-orange) ![Pipeline](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/badges/main/pipeline.svg) [![Coverage](https://sonar.fbe-adswen.rwu.de/api/project_badges/measure?project=de.rwu%3Aeasydrop&metric=coverage&token=sqb_2fe80aed361468170aaef32a0ff96d596456cdd1)](https://sonar.fbe-adswen.rwu.de/dashboard?id=de.rwu%3Aeasydrop) ![JAMANN](https://img.shields.io/badge/Auszahlung-Letzte%20Woche-brightgreen) @@ -13,30 +13,37 @@ Get rich quick™ with fully automated dropshipping! ![Demo Image](Docs/DemoImage.png) -## Installation ⚙ +## Installation/Usage ⚙ -As the application is still in development, development software will be required along its regular execution environment. - -- JDK 17 -- Maven -- _[More/context](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/wikis/Projektdokumentation/Development#software)_ - -## Usage 🛠 +- Install Docker + Compose +- Clone this repository Set up the required configuration files, use the corresponding demo files for orientation. - `config/config.properties` for API authorization - `products-config.json` to define product catalogues to use for dropshipping -Run the following command to execute the program: +Create the container -```sh -mvn compile exec:java -Dexec.mainClass="de.rwu.easydrop.Main" +```bash +docker-compose build +``` + +Then run it! + +```bash +docker-compose up +``` + +To run it on a fixed schedule, install a cron job, e.g. + +```bash +*/30 * * * * cd /home/user/easydrop && docker-compose up -d ``` ## Roadmap 🏁 -_Future ideas!_ +- Access transaction log ## Contributing 👷‍♂️👷‍♀️ diff --git a/Script/Bump_Version.sh b/Script/Bump_Version.sh new file mode 100644 index 0000000..190506d --- /dev/null +++ b/Script/Bump_Version.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Checking if correct number of arguments are provided +if [ "$#" -ne 2 ]; then + echo "Required CLI args missing: [old_version, new_version]" + exit 1 +fi + +# Assigning command line arguments to variables +old_version="$1" +new_version="$2" + +old_version_alt=${old_version//-/–} +new_version_alt=${new_version//-/–} + +# Replacing old_version with new_version in README.md +sed -i "s/version-$old_version_alt-orange/version-$new_version_alt-orange/g" README.md +sed -i "s/$old_version/$new_version/g" pom.xml +sed -i "s/easydrop-$old_version.jar/easydrop-$new_version.jar/g" Dockerfile +sed -i "s/image: easydrop:$old_version/image: easydrop:$new_version/g" docker-compose.yml + +echo "Version number updated successfully" + diff --git a/Script/SonarQube_Local.sh b/Script/SonarQube_Local.sh index 14d2602..8517553 100644 --- a/Script/SonarQube_Local.sh +++ b/Script/SonarQube_Local.sh @@ -2,6 +2,5 @@ mvn clean verify sonar:sonar -Pcoverage \ -Dsonar.projectKey=EasyDrop \ -Dsonar.projectName='EasyDrop' \ -Dsonar.host.url=http://localhost:9000 \ - -Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931 - -start http://localhost:9000/dashboard?id=EasyDrop \ No newline at end of file + -Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931 \ +&& start http://localhost:9000/dashboard?id=EasyDrop \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fe6a261 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.8" + +services: + easydrop: + container_name: easydrop + build: + context: . + dockerfile: Dockerfile + image: easydrop:0.3.0-SNAPSHOT + volumes: + - ./config:/config diff --git a/pom.xml b/pom.xml index 587ec28..6d1d8ff 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,27 @@ + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + de.rwu.easydrop.Main + + + + + + org.apache.maven.plugins maven-checkstyle-plugin From 554cc99b6dd4905fb07cf321c3e3749d2388494f Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 02:00:35 +0200 Subject: [PATCH 07/71] Removed needless classes --- src/main/java/de/rwu/easydrop/api/BeforeEach.java | 5 ----- src/main/java/de/rwu/easydrop/api/Test.java | 5 ----- 2 files changed, 10 deletions(-) delete mode 100644 src/main/java/de/rwu/easydrop/api/BeforeEach.java delete mode 100644 src/main/java/de/rwu/easydrop/api/Test.java diff --git a/src/main/java/de/rwu/easydrop/api/BeforeEach.java b/src/main/java/de/rwu/easydrop/api/BeforeEach.java deleted file mode 100644 index b316851..0000000 --- a/src/main/java/de/rwu/easydrop/api/BeforeEach.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.rwu.easydrop.api; - -public @interface BeforeEach { - -} diff --git a/src/main/java/de/rwu/easydrop/api/Test.java b/src/main/java/de/rwu/easydrop/api/Test.java deleted file mode 100644 index ca98b8c..0000000 --- a/src/main/java/de/rwu/easydrop/api/Test.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.rwu.easydrop.api; - -public @interface Test { - -} From e771063c283b9bd20ea02e40cbbc9a76a144d38f Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 02:00:51 +0200 Subject: [PATCH 08/71] Removed unused attribute --- src/main/java/de/rwu/easydrop/model/ProductCatalogue.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java index 7f0ad97..97f31dc 100644 --- a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java +++ b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java @@ -24,7 +24,6 @@ public class ProductCatalogue { * Product collection. */ private List products; - public float getProducts; /** * Creates new Product Catalogue. From 124d06a6bc325051acbbcbd11f9255f39f0e8ee7 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 02:11:36 +0200 Subject: [PATCH 09/71] Removed needless stump --- .../rwu/easydrop/service/processing/OrderManager.java | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/main/java/de/rwu/easydrop/service/processing/OrderManager.java diff --git a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java deleted file mode 100644 index 4d373b2..0000000 --- a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.service.processing; - -/** - * Processes dropshipping orders. - * - * TODO implement - */ -public class OrderManager { - -} From d3f89b726a7869a906b94e7d226b974fe7d9088f Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 02:14:41 +0200 Subject: [PATCH 10/71] Revert "Removed needless stump" This reverts commit 124d06a6bc325051acbbcbd11f9255f39f0e8ee7. --- .../rwu/easydrop/service/processing/OrderManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/de/rwu/easydrop/service/processing/OrderManager.java 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 { + +} From 5f6fa3fec9598ad0afc938a304067459d6b21a14 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:04:32 +0200 Subject: [PATCH 11/71] #11 Rewrote, moved code to OrderManager --- .../java/de/rwu/easydrop/PriceFetcher.java | 96 ------------------- .../service/processing/OrderManager.java | 90 +++++++++++++++-- 2 files changed, 84 insertions(+), 102 deletions(-) delete mode 100644 src/main/java/de/rwu/easydrop/PriceFetcher.java diff --git a/src/main/java/de/rwu/easydrop/PriceFetcher.java b/src/main/java/de/rwu/easydrop/PriceFetcher.java deleted file mode 100644 index 4ccf2e5..0000000 --- a/src/main/java/de/rwu/easydrop/PriceFetcher.java +++ /dev/null @@ -1,96 +0,0 @@ -package de.rwu.easydrop; - -import java.net.MalformedURLException; -import java.util.ArrayList; -import java.util.List; - -import javax.naming.ConfigurationException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sqlite.SQLiteDataSource; - -import de.rwu.easydrop.api.client.DataSourceFactory; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; -import de.rwu.easydrop.data.connector.SQLiteConnector; -import de.rwu.easydrop.model.Product; -import de.rwu.easydrop.model.ProductCatalogue; -import de.rwu.easydrop.service.retriever.CatalogueRetriever; -import de.rwu.easydrop.service.retriever.ProductRetriever; -import de.rwu.easydrop.service.writer.CatalogueWriter; -import de.rwu.easydrop.util.Config; -import de.rwu.easydrop.util.ProductsConfig; - - -/** - * Diese Klasse PriceFetcher enthält die Methode getProductPrice, - * die den Preis eines Produkts von den jeweiligen API's abruft. - */ -public final class PriceFetcher { - static List api1Prices = new ArrayList<>(); - static List api2Prices = new ArrayList<>(); - - private static final Logger LOGGER = LoggerFactory.getLogger(PriceFetcher.class); - - //Methode, um Preise der Prdukte zu vergleichen -> MArgen ermitteln - public static void comparePrices() throws ConfigurationException { - List api1Prices = new ArrayList<>(); - List api2Prices = new ArrayList<>(); - - for (int i = 0; i < api1Prices.size(); i++) { - double api1Price = api1Prices.get(i); - double api2Price = api2Prices.get(i); - String productId = "Product " + (i + 1); - - if (api1Price < api2Price) { - double priceDifference = api2Price - api1Price; - LOGGER.info("API 1 hat einen günstigeren Preis für " + productId + - ". Der Preisunterschied beträgt: " + priceDifference); - } else if (api2Price < api1Price) { - double priceDifference = api1Price - api2Price; - LOGGER.info("API 2 hat einen günstigeren Preis für " + productId + - ". Der Preisunterschied beträgt: " + priceDifference); - } else { - LOGGER.info("Beide APIs haben den gleichen Preis für " + productId); - } - } -} - - // Methode, um Preise der Produkte zu ermitteln - public static void getProductPrice(final String[] args) throws ConfigurationException{ - - Config config = Config.getInstance(); - ProductsConfig pConfig = ProductsConfig.getInstance(); - DataSourceFactory dataSourceFactory = new DataSourceFactory(config); - ProductRetriever retriever = new ProductRetriever(dataSourceFactory); - CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); - AbstractProductPersistence db = new SQLiteConnector(new SQLiteDataSource()); - CatalogueWriter catWriter = new CatalogueWriter(db); - - catRetriever.loadCatalogues(); - List pCats = catRetriever.getProductCatalogues(); - catWriter.writeCatalogues(pCats); - - for (ProductCatalogue pCat : pCats) { - String pCatStr = pCat.toString(); - LOGGER.info(pCatStr); - } - - for (ProductCatalogue pCat : pCats) { - List products = pCat.getProducts(); - - for (Product product : products) { - String productId = product.getProductId(); - double price = product.getCurrentPrice(); - LOGGER.info("Product ID: " + productId + ", Price: " + price); - if (productId == "B096Y2TYKV") { - api1Prices.add(price); - } else if (productId == "Gigabyte GeForce RTX 3060") { - api2Prices.add(price); - } - } - } - } - - -} diff --git a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java index 4d373b2..c84640f 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java @@ -1,10 +1,88 @@ package de.rwu.easydrop.service.processing; -/** - * Processes dropshipping orders. - * - * TODO implement - */ -public class OrderManager { +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.rwu.easydrop.Main; +import de.rwu.easydrop.exception.InvalidCatalogueException; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.ProductPair; +import de.rwu.easydrop.util.FormattingUtil; + +/** + * Creates dropshipping orders based on price margin. + * + * @since 0.3.0 + */ +public final class OrderManager { + + /** + * Temporary logging instance. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(OrderManager.class); + + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private OrderManager() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a stateless class, don't instantiate it."); + } + + /** + * Creates orders for products with sufficient margin. + * + * @param pCats Product Catalogues + */ + public static void createOrders(final List pCats) { + for (ProductCatalogue pCat : pCats) { + ProductPair pair = getHighestMarginProducts(pCat); + + // TODO: create actual orders/transactions, remove logger + double margin = pair.getProduct2().getCurrentPrice() - + pair.getProduct1().getCurrentPrice(); + String marginFormatted = FormattingUtil.formatEuro(margin); + LOGGER.info("{}: Margin {} ({} to {})", + pCat.getProductName(), + marginFormatted, + pair.getProduct1().getDataOrigin(), + pair.getProduct2().getDataOrigin()); + } + } + + /** + * Returns the cheapest and most expensive product of a catalogue to guarantee + * the biggest margin as a pair. + * + * @param pCat Product Catalogue + * @return Cheapest Product, Most Expensive Product as a Product Pair + */ + public static ProductPair getHighestMarginProducts(final ProductCatalogue pCat) { + if (pCat.getProducts().size() < 2) { + throw new InvalidCatalogueException("Product Catalogue holds less than 2 products!"); + } + + // Initialize indexes + Product cheapestProduct = pCat.getProducts().get(0); + Product mostExpensiveProduct = pCat.getProducts().get(0); + + for (Product product : pCat.getProducts()) { + if (product.getCurrentPrice() < cheapestProduct.getCurrentPrice()) { + cheapestProduct = product; + } + if (product.getCurrentPrice() > mostExpensiveProduct.getCurrentPrice()) { + mostExpensiveProduct = product; + } + } + + if (cheapestProduct.getCurrentPrice() == mostExpensiveProduct.getCurrentPrice()) { + throw new InvalidCatalogueException("Price margin is zero!"); + } + + return new ProductPair(cheapestProduct, mostExpensiveProduct); + } } From 4c3f6ebe82e24f38dbe30840c830b22d2a0cbee6 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:04:56 +0200 Subject: [PATCH 12/71] #11 Added supporting class and exception --- .../exception/InvalidCatalogueException.java | 27 ++++++++++++++++ .../de/rwu/easydrop/model/ProductPair.java | 31 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java create mode 100644 src/main/java/de/rwu/easydrop/model/ProductPair.java diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java b/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java new file mode 100644 index 0000000..69e582a --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies the data of a Product are invalid. + * + * @since 0.3.0 + */ +public class InvalidCatalogueException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public InvalidCatalogueException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public InvalidCatalogueException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/model/ProductPair.java b/src/main/java/de/rwu/easydrop/model/ProductPair.java new file mode 100644 index 0000000..70140c0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/ProductPair.java @@ -0,0 +1,31 @@ +package de.rwu.easydrop.model; + +import lombok.Data; + +/** + * Associates two related Products to one another. + * + * @since 0.3.0 + */ +@Data +public class ProductPair { + /** + * The first product. + */ + private final Product product1; + /** + * The second product. + */ + private final Product product2; + + /** + * Constructs a Product Pair. + * + * @param a First product + * @param b Second product + */ + public ProductPair(final Product a, final Product b) { + this.product1 = a; + this.product2 = b; + } +} From e593290f3cfc12fa6e1610f393923352a0881f40 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:05:13 +0200 Subject: [PATCH 13/71] Updated Main --- src/main/java/de/rwu/easydrop/Main.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index efc9a60..9a8d152 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -1,6 +1,5 @@ package de.rwu.easydrop; -import java.net.MalformedURLException; import java.util.List; import javax.naming.ConfigurationException; @@ -14,6 +13,7 @@ import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.data.connector.SQLiteConnector; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.processing.OrderManager; import de.rwu.easydrop.service.retriever.CatalogueRetriever; import de.rwu.easydrop.service.retriever.ProductRetriever; import de.rwu.easydrop.service.writer.CatalogueWriter; @@ -61,14 +61,6 @@ public final class Main { LOGGER.info(pCatStr); } - for (ProductCatalogue pCat : pCats) { - List products = pCat.getProducts(); - - for (Product product : products) { - String productId = product.getProductId(); - double price = product.getCurrentPrice(); - LOGGER.info("Product ID: " + productId + ", Price: " + price); - } - } + OrderManager.createOrders(pCats); } } From cba6e46de734b1a36c160b69a3179be08c6fe5f9 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:16:29 +0200 Subject: [PATCH 14/71] #11 Updated and moved tests --- .../easydrop/api/AmazonPriceFetcherTest.java | 56 ------------ .../service/processing/OrderManagerTest.java | 91 +++++++++++++++++++ 2 files changed, 91 insertions(+), 56 deletions(-) delete mode 100644 src/test/java/de/rwu/easydrop/api/AmazonPriceFetcherTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java diff --git a/src/test/java/de/rwu/easydrop/api/AmazonPriceFetcherTest.java b/src/test/java/de/rwu/easydrop/api/AmazonPriceFetcherTest.java deleted file mode 100644 index 4be1200..0000000 --- a/src/test/java/de/rwu/easydrop/api/AmazonPriceFetcherTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.rwu.easydrop.api; - -import java.net.MalformedURLException; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class AmazonPriceFetcherTest { - private static final String BASE_URL = "https://api.amazon.com"; - private static final String API_KEY = "your_api_key"; - private static final String PRODUCT_ID = "12345"; - - private AmazonPriceFetcher priceFetcher; - - @BeforeEach - public void setup() { - priceFetcher = new AmazonPriceFetcher(BASE_URL, API_KEY); - } - - @Test - public void testGetProductPrice_ValidProductId_ReturnsPrice() throws MalformedURLException { - // Arrange - double expectedPrice = -1.0; - - // Act - double actualPrice = priceFetcher.getProductPrice(PRODUCT_ID); - - // Assert - Assertions.assertEquals(expectedPrice, actualPrice); - } - - @Test - public void testGetProductPrice_InvalidProductId_ReturnsNegativePrice() throws MalformedURLException { - // Arrange - double expectedPrice = -1.0; - - // Act - double actualPrice = priceFetcher.getProductPrice("invalid_product_id"); - - // Assert - Assertions.assertEquals(expectedPrice, actualPrice); - } - - @Test - public void testGetProductPrice_ExceptionThrown_ReturnsNegativePrice() throws MalformedURLException { - // Arrange - double expectedPrice = -1.0; - - // Act - double actualPrice = priceFetcher.getProductPrice("exception_thrown"); - - // Assert - Assertions.assertEquals(expectedPrice, actualPrice); - } -} \ No newline at end of file diff --git a/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java b/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java new file mode 100644 index 0000000..a83fdc8 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java @@ -0,0 +1,91 @@ +package de.rwu.easydrop.service.processing; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.exception.InvalidCatalogueException; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.ProductPair; + +class OrderManagerTest { + + @Test + void testCreateOrders_ValidCatalogues_OrdersCreated() { + // Arrange + List catalogues = createSampleCatalogues(); + + // Act + OrderManager.createOrders(catalogues); + + // No assertions, just checking if the orders are created successfully + } + + @Test + void testGetHighestMarginProducts_ValidCatalogue_CheapestAndMostExpensiveProductsReturned() { + // Arrange + ProductCatalogue catalogue = createSampleCatalogue(); + + // Act + ProductPair pair = OrderManager.getHighestMarginProducts(catalogue); + + // Assert + Assertions.assertNotNull(pair); + Assertions.assertNotNull(pair.getProduct1()); + Assertions.assertNotNull(pair.getProduct2()); + Assertions.assertEquals("Product1", pair.getProduct1().getProductId()); + Assertions.assertEquals("Product3", pair.getProduct2().getProductId()); + } + + @Test + void testGetHighestMarginProducts_InvalidCatalogue_ThrowsInvalidCatalogueException() { + // Arrange + ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); + catalogue.addProduct(new Product()); + // The catalogue has only one product, which is invalid + + // Act & Assert + Assertions.assertThrows(InvalidCatalogueException.class, + () -> OrderManager.getHighestMarginProducts(catalogue)); + } + + private List createSampleCatalogues() { + List catalogues = new ArrayList<>(); + + ProductCatalogue catalogue1 = createSampleCatalogue(); + catalogues.add(catalogue1); + + ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue2", "Sample catalogue 2"); + Product product4 = new Product(); + product4.setProductId("Product4"); + product4.setCurrentPrice(6.78); + catalogue2.addProduct(product4); + Product product5 = new Product(); + product5.setProductId("Product5"); + product5.setCurrentPrice(7.89); + catalogue2.addProduct(product5); + catalogues.add(catalogue2); + + return catalogues; + } + + private ProductCatalogue createSampleCatalogue() { + ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); + Product product1 = new Product(); + product1.setProductId("Product1"); + product1.setCurrentPrice(1.23); + catalogue.addProduct(product1); + Product product2 = new Product(); + product2.setProductId("Product2"); + product2.setCurrentPrice(2.34); + catalogue.addProduct(product2); + Product product3 = new Product(); + product3.setProductId("Product3"); + product3.setCurrentPrice(4.56); + catalogue.addProduct(product3); + return catalogue; + } +} From 7c00e25e56946d89677b6d82234a85b1a1bc3f03 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:16:43 +0200 Subject: [PATCH 15/71] Checkstyle adjustments --- .../de/rwu/easydrop/service/processing/OrderManager.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java index c84640f..d841505 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java @@ -5,7 +5,6 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import de.rwu.easydrop.Main; import de.rwu.easydrop.exception.InvalidCatalogueException; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; @@ -42,9 +41,9 @@ public final class OrderManager { for (ProductCatalogue pCat : pCats) { ProductPair pair = getHighestMarginProducts(pCat); - // TODO: create actual orders/transactions, remove logger - double margin = pair.getProduct2().getCurrentPrice() - - pair.getProduct1().getCurrentPrice(); + // #12: Create actual orders/transactions, remove logger + double margin = pair.getProduct2().getCurrentPrice() + - pair.getProduct1().getCurrentPrice(); String marginFormatted = FormattingUtil.formatEuro(margin); LOGGER.info("{}: Margin {} ({} to {})", pCat.getProductName(), From 8d9fa83cd7950f3aff01be09500eea0b9a3552a6 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:17:35 +0200 Subject: [PATCH 16/71] =?UTF-8?q?Replaced=20=E2=82=AC=20symbol=20with=20te?= =?UTF-8?q?xt=20for=20charset=20safety?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/de/rwu/easydrop/util/FormattingUtil.java | 2 +- src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java index 7ef0e69..e8ea662 100644 --- a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java +++ b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java @@ -25,7 +25,7 @@ public final class FormattingUtil { * @return formatted price */ public static String formatEuro(final double amount) { - return String.format(Locale.GERMAN, "%,.2f", amount) + " €"; + return String.format(Locale.GERMAN, "%,.2f", amount) + " Euro"; } /** diff --git a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java index 1c7d4f9..4b1dfa1 100644 --- a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java +++ b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java @@ -29,7 +29,7 @@ class FormattingUtilTest { @Test void testFormatEuro_positiveAmount() { double amount = 1234.56; - String expectedFormattedAmount = "1.234,56 €"; + String expectedFormattedAmount = "1.234,56 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount); @@ -39,7 +39,7 @@ class FormattingUtilTest { @Test void testFormatEuro_zeroAmount() { double amount = 0.0; - String expectedFormattedAmount = "0,00 €"; + String expectedFormattedAmount = "0,00 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount); @@ -49,7 +49,7 @@ class FormattingUtilTest { @Test void testFormatEuro_negativeAmount() { double amount = -789.12; - String expectedFormattedAmount = "-789,12 €"; + String expectedFormattedAmount = "-789,12 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount); From 83c6ffc3f18a8a0009c4fb60f2c4214405858852 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:17:51 +0200 Subject: [PATCH 17/71] Removed useless import --- src/main/java/de/rwu/easydrop/Main.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index 9a8d152..87a23a5 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -11,7 +11,6 @@ import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.data.connector.SQLiteConnector; -import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.service.processing.OrderManager; import de.rwu.easydrop.service.retriever.CatalogueRetriever; From 5edc523c6e849966903894f90af09b10e2ea667e Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:20:18 +0200 Subject: [PATCH 18/71] =?UTF-8?q?Replaced=20remaining=20=E2=82=AC=20refere?= =?UTF-8?q?nces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java | 4 ++-- src/test/java/de/rwu/easydrop/model/ProductTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java index f070e2d..c13dcfe 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java @@ -85,8 +85,8 @@ class ProductCatalogueTest { String expectedString = "Product Catalogue: GPU\n" + "Description: Graphics Processing Units\n" + "Products:\n" + - "Product: [12345 from AmazonSeller (Amazon) at 0,00 € (available: no)]\n" + - "Product: [54321 from eBaySeller (eBay) at 0,00 € (available: no)]\n"; + "Product: [12345 from AmazonSeller (Amazon) at 0,00 Euro (available: no)]\n" + + "Product: [54321 from eBaySeller (eBay) at 0,00 Euro (available: no)]\n"; Assertions.assertEquals(expectedString, productCatalogue.toString()); } diff --git a/src/test/java/de/rwu/easydrop/model/ProductTest.java b/src/test/java/de/rwu/easydrop/model/ProductTest.java index 1845b65..cf79119 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductTest.java @@ -16,7 +16,7 @@ class ProductTest { product1.setCurrentPrice(19.99); product1.setAvailable(true); - String expectedString1 = "Product: [12345 from Merchant A (Amazon) at 19,99 € (available: yes)]"; + String expectedString1 = "Product: [12345 from Merchant A (Amazon) at 19,99 Euro (available: yes)]"; String result1 = product1.toString(); assertEquals(expectedString1, result1); @@ -31,7 +31,7 @@ class ProductTest { product2.setCurrentPrice(9.99); product2.setAvailable(false); - String expectedString2 = "Product: [67890 from Merchant B (eBay) at 9,99 € (available: no)]"; + String expectedString2 = "Product: [67890 from Merchant B (eBay) at 9,99 Euro (available: no)]"; String result2 = product2.toString(); assertEquals(expectedString2, result2); From 7bfee9d719aeafb5514cf78dc820273763a8ab38 Mon Sep 17 00:00:00 2001 From: Shan Ruhhammer Date: Sun, 25 Jun 2023 03:28:52 +0200 Subject: [PATCH 19/71] Added more tests --- .../InvalidCatalogueExceptionTest.java | 33 ++++++++++ .../service/processing/OrderManagerTest.java | 61 +++++++++++++++---- 2 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java diff --git a/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java new file mode 100644 index 0000000..684bc57 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java @@ -0,0 +1,33 @@ +package de.rwu.easydrop.exception; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class InvalidCatalogueExceptionTest { + + @Test + void testConstructorWithMessage_ExceptionWithMessageCreated() { + // Arrange + String message = "Invalid catalogue"; + + // Act + InvalidCatalogueException exception = new InvalidCatalogueException(message); + + // Assert + Assertions.assertEquals(message, exception.getMessage()); + } + + @Test + void testConstructorWithMessageAndCause_ExceptionWithMessageAndCauseCreated() { + // Arrange + String message = "Invalid catalogue"; + Throwable cause = new RuntimeException("Cause exception"); + + // Act + InvalidCatalogueException exception = new InvalidCatalogueException(message, cause); + + // Assert + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertEquals(cause, exception.getCause()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java b/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java index a83fdc8..2ffb76a 100644 --- a/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java +++ b/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java @@ -1,9 +1,17 @@ package de.rwu.easydrop.service.processing; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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 java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import de.rwu.easydrop.exception.InvalidCatalogueException; @@ -13,15 +21,29 @@ import de.rwu.easydrop.model.ProductPair; class OrderManagerTest { + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = OrderManager.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + @Test void testCreateOrders_ValidCatalogues_OrdersCreated() { // Arrange List catalogues = createSampleCatalogues(); // Act - OrderManager.createOrders(catalogues); - - // No assertions, just checking if the orders are created successfully + assertDoesNotThrow(() -> { + OrderManager.createOrders(catalogues); + }); } @Test @@ -33,11 +55,11 @@ class OrderManagerTest { ProductPair pair = OrderManager.getHighestMarginProducts(catalogue); // Assert - Assertions.assertNotNull(pair); - Assertions.assertNotNull(pair.getProduct1()); - Assertions.assertNotNull(pair.getProduct2()); - Assertions.assertEquals("Product1", pair.getProduct1().getProductId()); - Assertions.assertEquals("Product3", pair.getProduct2().getProductId()); + assertNotNull(pair); + assertNotNull(pair.getProduct1()); + assertNotNull(pair.getProduct2()); + assertEquals("Product2", pair.getProduct1().getProductId()); + assertEquals("Product3", pair.getProduct2().getProductId()); } @Test @@ -48,8 +70,25 @@ class OrderManagerTest { // The catalogue has only one product, which is invalid // Act & Assert - Assertions.assertThrows(InvalidCatalogueException.class, + Exception e = assertThrows(InvalidCatalogueException.class, () -> OrderManager.getHighestMarginProducts(catalogue)); + + assertEquals("Product Catalogue holds less than 2 products!", e.getMessage()); + } + + @Test + void testGetHighestMarginProducts_InvalidCatalogue_NoMargin() { + // Arrange + ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); + catalogue.addProduct(new Product()); + catalogue.addProduct(new Product()); + // The catalogue has only one product, which is invalid + + // Act & Assert + Exception e = assertThrows(InvalidCatalogueException.class, + () -> OrderManager.getHighestMarginProducts(catalogue)); + + assertEquals("Price margin is zero!", e.getMessage()); } private List createSampleCatalogues() { @@ -80,7 +119,7 @@ class OrderManagerTest { catalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("Product2"); - product2.setCurrentPrice(2.34); + product2.setCurrentPrice(0.89); catalogue.addProduct(product2); Product product3 = new Product(); product3.setProductId("Product3"); From 4c283f5ca639f01a4e63e2943dd6d86bb6a488eb Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Fri, 9 Jun 2023 13:39:05 +0200 Subject: [PATCH 20/71] added general logic for easydrop core --- src/main/java/de/rwu/easydrop/core/Core.java | 34 ++++++++++++ .../de/rwu/easydrop/core/OfferIdentifier.java | 31 +++++++++++ .../rwu/easydrop/core/OfferProvisioner.java | 24 +++++++++ .../de/rwu/easydrop/core/OfferReviewer.java | 23 ++++++++ .../de/rwu/easydrop/core/OfferUpdater.java | 21 ++++++++ .../java/de/rwu/easydrop/model/Offer.java | 52 +++++++++++++++++++ 6 files changed, 185 insertions(+) create mode 100644 src/main/java/de/rwu/easydrop/core/Core.java create mode 100644 src/main/java/de/rwu/easydrop/core/OfferIdentifier.java create mode 100644 src/main/java/de/rwu/easydrop/core/OfferProvisioner.java create mode 100644 src/main/java/de/rwu/easydrop/core/OfferReviewer.java create mode 100644 src/main/java/de/rwu/easydrop/core/OfferUpdater.java create mode 100644 src/main/java/de/rwu/easydrop/model/Offer.java diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java new file mode 100644 index 0000000..b242b44 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -0,0 +1,34 @@ +package de.rwu.easydrop.core; + +import java.util.List; +import de.rwu.easydrop.model.Offer; + +public class Core { + + private OfferIdentifier ident; + private OfferProvisioner provis; + private OfferReviewer review; + private OfferUpdater update; + + + public Core(){ + this.ident = new OfferIdentifier(); + this.provis = new OfferProvisioner(); + this.review = new OfferReviewer(); + this.update = new OfferUpdater(); + + } + + + public void runCore(){ + List identifiedOffers = ident.runIdentifier(); + provis.runProvisioner(identifiedOffers); + List updatingOffers = review.runReviewer(); + update.runUpdater(updatingOffers); + + + + } + + +} diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java new file mode 100644 index 0000000..1d20353 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java @@ -0,0 +1,31 @@ +package de.rwu.easydrop.core; + +import de.rwu.easydrop.model.Offer; +import java.util.List; +import java.util.ArrayList; + + + +public class OfferIdentifier { + + + + + public OfferIdentifier() { + + } + + public List runIdentifier() { + /* muss die Kataloge durchforsten nach vergleichbaren Produkten auf mehreren Händlerwebseiten (APIs) + * muss Preisermittler als Abhängigkeit für jeden Katalog aufrufen + * erhält dadurch Liste von Produkten, die auf verschiedenen Plattformen eingestellt werden können, gibt diese Liste zurück + * muss Datenbank nach Produktangeboten durchforsten, ob diese Produkte schon eingestellt wurden vom OfferProvisioner + * wenn ja, muss er die Liste um diese Angebote kürzen + * + */ + return new ArrayList(); + + } + + +} diff --git a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java new file mode 100644 index 0000000..3b6f9bb --- /dev/null +++ b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java @@ -0,0 +1,24 @@ +package de.rwu.easydrop.core; + +import de.rwu.easydrop.model.Offer; + +import java.util.List; + +public class OfferProvisioner { + + public OfferProvisioner(/*OfferWriter for database? */) { + + } + + public void runProvisioner(List offersToProvision) { + /* + * Bekommt vom Identifier eine Liste mit Angeboten, die er einstellen soll auf Zielplattformen + * Stellt Angebote bei der Zielplattform ein + * Schreibt eingestellte Angebote in die Datenbank + */ + + + + } + +} diff --git a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java new file mode 100644 index 0000000..f410f41 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java @@ -0,0 +1,23 @@ +package de.rwu.easydrop.core; + +import de.rwu.easydrop.model.Offer; +import java.util.List; +import java.util.ArrayList; + +public class OfferReviewer { + + public OfferReviewer(/*OfferReader/retriever for database? */){ + + } + + public List runReviewer() { + /* + * Liest eingestellte Angebote in der Datenbank + * Prüft Zielplattformen der SourceProducts, ob diese noch verfügbar sind (Issue#12) bzw. ob sie sich im Preis geändert haben + * Gibt Liste zurück von Offers, die geändert werden müssen, wird übergeben an OfferUpdater mit availability true oder false + */ + return new ArrayList(); + + } + +} diff --git a/src/main/java/de/rwu/easydrop/core/OfferUpdater.java b/src/main/java/de/rwu/easydrop/core/OfferUpdater.java new file mode 100644 index 0000000..33091cb --- /dev/null +++ b/src/main/java/de/rwu/easydrop/core/OfferUpdater.java @@ -0,0 +1,21 @@ +package de.rwu.easydrop.core; + +import java.util.List; +import de.rwu.easydrop.model.Offer; + +public class OfferUpdater { + + public OfferUpdater() { + + } + + public void runUpdater(List offersToUpdate) { + /* + * Bekommt vom OfferReviewer eine Liste mit zu ändernden Offers übergeben. + * Bei availability=false löscht der Updater das Angebot aus der Datenbank und von der Zielplattform. + * Der Updater ändert die geänderten Parameter in der Liste in der Datenbank und in der Zielplattform ab. + * Er ändert das upDate in der Datenbank nach der Änderung. + */ + + } +} diff --git a/src/main/java/de/rwu/easydrop/model/Offer.java b/src/main/java/de/rwu/easydrop/model/Offer.java new file mode 100644 index 0000000..f1772d6 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/Offer.java @@ -0,0 +1,52 @@ +package de.rwu.easydrop.model; + +import java.util.Date; + + +import lombok.Data; + +/** + * An Offer. + * + * @since 0.3.0 + */ + +@Data +public class Offer { + /* + * The product that our software buys + */ + Product sourceProduct; + + /* + * The product that our software sells + */ + Product saleProduct; + + /* + * Boolean to check if sourceProduct is still available on source website + */ + boolean availability; + + /* + * Date of the creation of the offer + * NOTE: Use Timestamp? https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html + */ + Date creationDate; + + /* + * Date of last update of the offer on the destination website (API) + */ + Date upDate; + + /* + * Date of last check if offer is still valid + */ + Date checkDate; + + + + + + +} From 9906ede6ec578bf83d0864ebbcc73530c6f748c6 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Fri, 9 Jun 2023 13:41:23 +0200 Subject: [PATCH 21/71] added tests for easydrop core --- src/test/java/de/rwu/easydrop/core/CoreTest.java | 10 ++++++++++ .../java/de/rwu/easydrop/core/OfferIdentifierTest.java | 10 ++++++++++ .../de/rwu/easydrop/core/OfferProvisionerTest.java | 10 ++++++++++ .../java/de/rwu/easydrop/core/OfferReviewerTest.java | 10 ++++++++++ .../java/de/rwu/easydrop/core/OfferUpdaterTest.java | 10 ++++++++++ 5 files changed, 50 insertions(+) create mode 100644 src/test/java/de/rwu/easydrop/core/CoreTest.java create mode 100644 src/test/java/de/rwu/easydrop/core/OfferIdentifierTest.java create mode 100644 src/test/java/de/rwu/easydrop/core/OfferProvisionerTest.java create mode 100644 src/test/java/de/rwu/easydrop/core/OfferReviewerTest.java create mode 100644 src/test/java/de/rwu/easydrop/core/OfferUpdaterTest.java diff --git a/src/test/java/de/rwu/easydrop/core/CoreTest.java b/src/test/java/de/rwu/easydrop/core/CoreTest.java new file mode 100644 index 0000000..b81a7ac --- /dev/null +++ b/src/test/java/de/rwu/easydrop/core/CoreTest.java @@ -0,0 +1,10 @@ +package de.rwu.easydrop.core; + +import org.junit.jupiter.api.Test; + +public class CoreTest { + @Test + void testRunCore() { + + } +} diff --git a/src/test/java/de/rwu/easydrop/core/OfferIdentifierTest.java b/src/test/java/de/rwu/easydrop/core/OfferIdentifierTest.java new file mode 100644 index 0000000..92d7876 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/core/OfferIdentifierTest.java @@ -0,0 +1,10 @@ +package de.rwu.easydrop.core; + +import org.junit.jupiter.api.Test; + +public class OfferIdentifierTest { + @Test + void testRunIdentifier() { + + } +} diff --git a/src/test/java/de/rwu/easydrop/core/OfferProvisionerTest.java b/src/test/java/de/rwu/easydrop/core/OfferProvisionerTest.java new file mode 100644 index 0000000..6b3b26a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/core/OfferProvisionerTest.java @@ -0,0 +1,10 @@ +package de.rwu.easydrop.core; + +import org.junit.jupiter.api.Test; + +public class OfferProvisionerTest { + @Test + void testRunProvisioner() { + + } +} diff --git a/src/test/java/de/rwu/easydrop/core/OfferReviewerTest.java b/src/test/java/de/rwu/easydrop/core/OfferReviewerTest.java new file mode 100644 index 0000000..9c03f5c --- /dev/null +++ b/src/test/java/de/rwu/easydrop/core/OfferReviewerTest.java @@ -0,0 +1,10 @@ +package de.rwu.easydrop.core; + +import org.junit.jupiter.api.Test; + +public class OfferReviewerTest { + @Test + void testRunReviewer() { + + } +} diff --git a/src/test/java/de/rwu/easydrop/core/OfferUpdaterTest.java b/src/test/java/de/rwu/easydrop/core/OfferUpdaterTest.java new file mode 100644 index 0000000..b7e46ae --- /dev/null +++ b/src/test/java/de/rwu/easydrop/core/OfferUpdaterTest.java @@ -0,0 +1,10 @@ +package de.rwu.easydrop.core; + +import org.junit.jupiter.api.Test; + +public class OfferUpdaterTest { + @Test + void testRunUpdater() { + + } +} From c89c8974cf1aa054bcd12ace57f1e5252efbaeaa Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Fri, 9 Jun 2023 17:23:28 +0200 Subject: [PATCH 22/71] Wrote OfferIdentifier and updated ProductRetriever --- config/demo.config.properties | 3 +- .../de/rwu/easydrop/core/OfferIdentifier.java | 53 +++++++++++++++++-- .../java/de/rwu/easydrop/model/Offer.java | 10 ---- .../service/retriever/OfferRetriever.java | 14 +++++ .../service/retriever/ProductRetriever.java | 24 +++++++++ 5 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java diff --git a/config/demo.config.properties b/config/demo.config.properties index d3e4d63..13b994e 100644 --- a/config/demo.config.properties +++ b/config/demo.config.properties @@ -2,4 +2,5 @@ AMAZON_API_URL= AMAZON_API_KEY= EBAY_API_URL= -EBAY_API_KEY= \ No newline at end of file +EBAY_API_KEY= +WEBSHOPS=amazon,ebay \ No newline at end of file diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java index 1d20353..bfdbd9c 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java +++ b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java @@ -1,6 +1,13 @@ package de.rwu.easydrop.core; import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.retriever.OfferRetriever; +import de.rwu.easydrop.service.retriever.ProductRetriever; +import de.rwu.easydrop.util.Config; + +import javax.naming.ConfigurationException; import java.util.List; import java.util.ArrayList; @@ -8,11 +15,14 @@ import java.util.ArrayList; public class OfferIdentifier { + String[] webshops; + OfferRetriever offerRetriever; + ProductRetriever productRetriever; - - public OfferIdentifier() { - + public OfferIdentifier() throws ConfigurationException { + Config config = Config.getInstance(); + this.webshops = config.getProperty("WEBSHOPS").split(","); } public List runIdentifier() { @@ -23,8 +33,43 @@ public class OfferIdentifier { * wenn ja, muss er die Liste um diese Angebote kürzen * */ - return new ArrayList(); + // Just an example here taken from demo.products-config.json; should be generic and configurable either via + // JSON or the database later. + ProductCatalogue catalogue = new ProductCatalogue("Gigabyte GeForce RTX 3060", "Very epic GPU"); + for(String webshop: this.webshops) { + // The product retriever should know what API with which ProductID to query; high-level components should not know about such + // things. + Product product = this.productRetriever.getProductFromDataSource(webshop, catalogue.getProductName()); + if(product != null) { + catalogue.addProduct(product); + } + } + + List identifiedOffers = new ArrayList<>(); + // Here we would call the price function that decides if is feasible to dropship the product and if, at which + // margin + // Offer possibleOffer = GenerateOfferFromCatalogue(catalogue); + // if(possibleOffer != null) { + // identifiedOffers.add(possibleOffer); + // } + + List newOffers = new ArrayList<>(); + List existingOffers = offerRetriever.loadOffers(); + for(Offer identifiedOffer: identifiedOffers) { + boolean isNew = true; + for(Offer existingOffer: existingOffers) { + if(existingOffer.getSourceProduct().getProductId().equals(identifiedOffer.getSourceProduct().getProductId())) { + isNew = false; + break; + } + } + if(isNew) { + newOffers.add(identifiedOffer); + } + } + + return newOffers; } diff --git a/src/main/java/de/rwu/easydrop/model/Offer.java b/src/main/java/de/rwu/easydrop/model/Offer.java index f1772d6..a030146 100644 --- a/src/main/java/de/rwu/easydrop/model/Offer.java +++ b/src/main/java/de/rwu/easydrop/model/Offer.java @@ -23,11 +23,6 @@ public class Offer { */ Product saleProduct; - /* - * Boolean to check if sourceProduct is still available on source website - */ - boolean availability; - /* * Date of the creation of the offer * NOTE: Use Timestamp? https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html @@ -44,9 +39,4 @@ public class Offer { */ Date checkDate; - - - - - } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java new file mode 100644 index 0000000..3571b7d --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java @@ -0,0 +1,14 @@ +package de.rwu.easydrop.service.retriever; + +import de.rwu.easydrop.model.Offer; + +import java.util.ArrayList; +import java.util.List; + +public class OfferRetriever { + + public List loadOffers() { + return new ArrayList<>(); + } + +} diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java index da842c3..d4e5950 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -81,4 +81,28 @@ public class ProductRetriever { return product; } + + /** + * Retrieves a product from an API by name of the API so that high-level components do not need to be extended + * with exact function call names if we extend the list of webshops. + * + * @param dataSourceName Data source name, e.g. amazon + * @param productName Product name, translated to the correct product ID for the data source + * @return Product from that data source or null if data source not available + */ + public Product getProductFromDataSource(final String dataSourceName, final String productName) { + switch(dataSourceName.toLowerCase()) { + case "amazon": + // TODO: Translation from productName to productId (Amazon) needed + return getProductFromAmazon(productName); + case "ebay": + // TODO: Translation from productName to productId (eBay) needed + return getProductFromEbay(productName); + case "persistence": + // TODO: Translation from productName to productId (persistence layer) needed + return getProductFromPersistence(productName); + default: + return null; + } + } } From 5e6a2541fa255ca02c52a7cf0e3a65707d622108 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Fri, 9 Jun 2023 17:24:46 +0200 Subject: [PATCH 23/71] Wrote OfferIdentifier and updated ProductRetriever --- src/main/java/de/rwu/easydrop/core/OfferIdentifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java index bfdbd9c..819ea8a 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java +++ b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java @@ -20,7 +20,7 @@ public class OfferIdentifier { ProductRetriever productRetriever; - public OfferIdentifier() throws ConfigurationException { + public OfferIdentifier() { Config config = Config.getInstance(); this.webshops = config.getProperty("WEBSHOPS").split(","); } From 5436118829591619f31de35b9e03a0f8cea2d291 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Thu, 15 Jun 2023 18:47:10 +0200 Subject: [PATCH 24/71] Wrote Enum for webshops changed code in respective classes --- .../java/de/rwu/easydrop/core/OfferIdentifier.java | 9 +++------ src/main/java/de/rwu/easydrop/model/Product.java | 7 +++++++ .../easydrop/service/retriever/ProductRetriever.java | 11 ++++------- src/test/java/de/rwu/easydrop/core/CoreTest.java | 2 ++ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java index 819ea8a..c52c5d1 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java +++ b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java @@ -15,15 +15,12 @@ import java.util.ArrayList; public class OfferIdentifier { - String[] webshops; + OfferRetriever offerRetriever; ProductRetriever productRetriever; - public OfferIdentifier() { - Config config = Config.getInstance(); - this.webshops = config.getProperty("WEBSHOPS").split(","); - } + public OfferIdentifier() {} public List runIdentifier() { /* muss die Kataloge durchforsten nach vergleichbaren Produkten auf mehreren Händlerwebseiten (APIs) @@ -37,7 +34,7 @@ public class OfferIdentifier { // Just an example here taken from demo.products-config.json; should be generic and configurable either via // JSON or the database later. ProductCatalogue catalogue = new ProductCatalogue("Gigabyte GeForce RTX 3060", "Very epic GPU"); - for(String webshop: this.webshops) { + for(Product.webshop webshop: Product.webshop.values()) { // The product retriever should know what API with which ProductID to query; high-level components should not know about such // things. Product product = this.productRetriever.getProductFromDataSource(webshop, catalogue.getProductName()); diff --git a/src/main/java/de/rwu/easydrop/model/Product.java b/src/main/java/de/rwu/easydrop/model/Product.java index 1e5b3b2..c8b70ff 100644 --- a/src/main/java/de/rwu/easydrop/model/Product.java +++ b/src/main/java/de/rwu/easydrop/model/Product.java @@ -10,6 +10,13 @@ import lombok.Data; */ @Data public class Product { + /* + * Constants for data source/destination platforms + */ + public enum webshop{ + AMAZON, EBAY + } + /** * Data source platform, like "Amazon". */ diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java index d4e5950..8b814f9 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -90,17 +90,14 @@ public class ProductRetriever { * @param productName Product name, translated to the correct product ID for the data source * @return Product from that data source or null if data source not available */ - public Product getProductFromDataSource(final String dataSourceName, final String productName) { - switch(dataSourceName.toLowerCase()) { - case "amazon": + public Product getProductFromDataSource(Product.webshop dataSourceName, final String productName) { + switch(dataSourceName) { + case AMAZON: // TODO: Translation from productName to productId (Amazon) needed return getProductFromAmazon(productName); - case "ebay": + case EBAY: // TODO: Translation from productName to productId (eBay) needed return getProductFromEbay(productName); - case "persistence": - // TODO: Translation from productName to productId (persistence layer) needed - return getProductFromPersistence(productName); default: return null; } diff --git a/src/test/java/de/rwu/easydrop/core/CoreTest.java b/src/test/java/de/rwu/easydrop/core/CoreTest.java index b81a7ac..55cb64c 100644 --- a/src/test/java/de/rwu/easydrop/core/CoreTest.java +++ b/src/test/java/de/rwu/easydrop/core/CoreTest.java @@ -5,6 +5,8 @@ import org.junit.jupiter.api.Test; public class CoreTest { @Test void testRunCore() { + Core core1=new Core(); + core1.runCore(); } } From 5204cc171090626b788b1b77633baadd8e573e33 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Thu, 15 Jun 2023 18:52:06 +0200 Subject: [PATCH 25/71] Wrote OfferWriter and following classes to save offers to persistence --- .../de/rwu/easydrop/api/dto/OfferDTO.java | 51 +++++++++++++++++++ .../rwu/easydrop/core/OfferProvisioner.java | 30 ++++++++++- .../connector/AbstractOfferPersistence.java | 28 ++++++++++ .../java/de/rwu/easydrop/model/Offer.java | 5 ++ .../easydrop/service/mapping/OfferMapper.java | 50 ++++++++++++++++++ .../service/validation/OfferValidator.java | 32 ++++++++++++ .../easydrop/service/writer/OfferWriter.java | 33 ++++++++++++ 7 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java create mode 100644 src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java create mode 100644 src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java create mode 100644 src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java create mode 100644 src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java diff --git a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java new file mode 100644 index 0000000..ee7d5f4 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java @@ -0,0 +1,51 @@ +package de.rwu.easydrop.api.dto; + +import lombok.Data; +import de.rwu.easydrop.model.Product; +import java.util.Date; + +/* + * Offer data transfer object + */ +@Data +public class OfferDTO { + /** + * Platform internal offer identifier. + */ + private String offerId; + + /* + * The product that our software buys + */ + Product sourceProduct; + + /* + * The product that our software sells + */ + Product saleProduct; + + /* + * Date of the creation of the offer + * NOTE: Use Timestamp? https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html + */ + Date creationDate; + + /* + * Date of last update of the offer on the destination website (API) + */ + Date upDate; + + /* + * Date of last check if offer is still valid + */ + Date checkDate; + + /** + * Creates OfferDTO instance. + * + * @param newOfferId Internal Offer identifier + */ + public OfferDTO(final String newOfferId) { + this.offerId = newOfferId; + } +} diff --git a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java index 3b6f9bb..27aa676 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java @@ -1,13 +1,25 @@ package de.rwu.easydrop.core; -import de.rwu.easydrop.model.Offer; - import java.util.List; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.service.retriever.ProductRetriever; +import de.rwu.easydrop.service.writer.OfferWriter; + public class OfferProvisioner { + ProductRetriever productRetriever; + // ProductWriter + AbstractProductPersistence db; + OfferWriter offerWriter; + public OfferProvisioner(/*OfferWriter for database? */) { + //in die Klasse hardcoden, "webshop" nicht verwenden + //oder eine Konstante anlegen die das halten würde (besser skalierbar) + //die Konstante in model anlegen (in Product, Konstante valid origins) + this.offerWriter = new OfferWriter(); } public void runProvisioner(List offersToProvision) { @@ -16,6 +28,20 @@ public class OfferProvisioner { * Stellt Angebote bei der Zielplattform ein * Schreibt eingestellte Angebote in die Datenbank */ + + for(Offer newOffer: offersToProvision){ + // if(newOffer.getSourceProduct().getDataOrigin().equals("ebay")){ + // //put newOffer on amazon + + // } + //else put newOffer on eBay + + // if successfully transmitted + // add to persistence + // "duplicate" the product with dataOrigin new platform and merchant = "me" + offerWriter.writeOfferToPersistence(newOffer); + } + diff --git a/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java b/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java new file mode 100644 index 0000000..4562a6d --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java @@ -0,0 +1,28 @@ +package de.rwu.easydrop.data.connector; + +import de.rwu.easydrop.api.dto.OfferDTO; + +public abstract class AbstractOfferPersistence { + /** + * Data origin. + */ + public static final String DATA_ORIGIN = "Persistence"; + + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + public abstract void saveOffer(OfferDTO dto); + + /** + * Gets a OfferDTO from persistence. + */ + //@Override + public abstract OfferDTO getOfferDTOById(String OfferId); + + /** + * Deletes all data from persistence. + */ + public abstract void clearData(); +} diff --git a/src/main/java/de/rwu/easydrop/model/Offer.java b/src/main/java/de/rwu/easydrop/model/Offer.java index a030146..174e1d4 100644 --- a/src/main/java/de/rwu/easydrop/model/Offer.java +++ b/src/main/java/de/rwu/easydrop/model/Offer.java @@ -39,4 +39,9 @@ public class Offer { */ Date checkDate; + /* + * ID of the offer + */ + String offerId; + } diff --git a/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java new file mode 100644 index 0000000..f6aeb73 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java @@ -0,0 +1,50 @@ +package de.rwu.easydrop.service.mapping; + +import de.rwu.easydrop.api.dto.OfferDTO; + +import de.rwu.easydrop.model.Offer; + +/** + * Maps between Offer, OfferDTO and OfferDTO. + * + * @since 0.2.0 + * + * @see Offer + * @see OfferDTO + * @see OfferDTO + */ +public final class OfferMapper { + + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private OfferMapper() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a mapping class, don't instantiate it."); + } + + /** + * Creates a Offer object from a corresponding DTO. + * + * @param dto Offer Data Transfer Object + * @return Offer + */ + public static Offer mapOfferFromDTO(final OfferDTO dto) { + Offer offer = new Offer(); + + return offer; + } + + /** + * Creates an OfferDTO object from a corresponding offer. + * + * @param Offer offer + * @return OfferDTO + */ + public static OfferDTO mapOfferToDTO(final Offer offer) { + OfferDTO dto = new OfferDTO(offer.getOfferId()); + + return dto; + } +} diff --git a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java new file mode 100644 index 0000000..2f466f1 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java @@ -0,0 +1,32 @@ +package de.rwu.easydrop.service.validation; +import java.util.HashSet; +import java.util.Set; + +import de.rwu.easydrop.exception.InvalidProductException; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Offer; + +/** + * Confirms validity of Offer data. + * + * @since 0.2.0 + */ +public class OfferValidator { + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private OfferValidator() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a validator class, don't instantiate it."); + } + /** + * Makes sure an Offer does not contain invalid information. + * + * @param offer the Offer + */ + public static void validate(final Offer offer) {} +} + + + diff --git a/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java b/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java new file mode 100644 index 0000000..8933d55 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java @@ -0,0 +1,33 @@ +package de.rwu.easydrop.service.writer; + +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.data.connector.AbstractOfferPersistence; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.service.mapping.OfferMapper; +import de.rwu.easydrop.service.validation.OfferValidator; + +public class OfferWriter { + /** + * Persistence. + */ + private AbstractOfferPersistence persistence; + + /** + * @param newPersistence the persistence to set + */ + public void setPersistence(final AbstractOfferPersistence newPersistence) { + this.persistence = newPersistence; + } + + /** + * Validates and saves product to persistence. + * + * @param product + */ + public void writeOfferToPersistence(final Offer offer) { + OfferValidator.validate(offer); + OfferDTO dto = OfferMapper.mapOfferToDTO(offer); + + persistence.saveOffer(dto); + } +} From fc4f5c6fc6fa0c75798d3ab2f8d84f40fbb02c9b Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Thu, 15 Jun 2023 19:11:30 +0200 Subject: [PATCH 26/71] fixed Tests --- src/main/java/de/rwu/easydrop/core/Core.java | 5 ++++- src/main/java/de/rwu/easydrop/core/OfferIdentifier.java | 7 ++++++- .../de/rwu/easydrop/service/validation/OfferValidator.java | 4 ---- src/test/java/de/rwu/easydrop/core/CoreTest.java | 4 +++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java index b242b44..82af9ef 100644 --- a/src/main/java/de/rwu/easydrop/core/Core.java +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -1,6 +1,9 @@ package de.rwu.easydrop.core; import java.util.List; + +import javax.naming.ConfigurationException; + import de.rwu.easydrop.model.Offer; public class Core { @@ -11,7 +14,7 @@ public class Core { private OfferUpdater update; - public Core(){ + public Core() throws ConfigurationException{ this.ident = new OfferIdentifier(); this.provis = new OfferProvisioner(); this.review = new OfferReviewer(); diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java index c52c5d1..2093052 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java +++ b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java @@ -1,5 +1,6 @@ package de.rwu.easydrop.core; +import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; @@ -20,7 +21,11 @@ public class OfferIdentifier { ProductRetriever productRetriever; - public OfferIdentifier() {} + public OfferIdentifier() throws ConfigurationException{ + this.offerRetriever = new OfferRetriever(); + DataSourceFactory dataSourceFactory = new DataSourceFactory(Config.getInstance()); + this.productRetriever= new ProductRetriever(dataSourceFactory); + } public List runIdentifier() { /* muss die Kataloge durchforsten nach vergleichbaren Produkten auf mehreren Händlerwebseiten (APIs) diff --git a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java index 2f466f1..ab6b26e 100644 --- a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java +++ b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java @@ -1,9 +1,5 @@ package de.rwu.easydrop.service.validation; -import java.util.HashSet; -import java.util.Set; -import de.rwu.easydrop.exception.InvalidProductException; -import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.Offer; /** diff --git a/src/test/java/de/rwu/easydrop/core/CoreTest.java b/src/test/java/de/rwu/easydrop/core/CoreTest.java index 55cb64c..5eb429c 100644 --- a/src/test/java/de/rwu/easydrop/core/CoreTest.java +++ b/src/test/java/de/rwu/easydrop/core/CoreTest.java @@ -1,10 +1,12 @@ package de.rwu.easydrop.core; +import javax.naming.ConfigurationException; + import org.junit.jupiter.api.Test; public class CoreTest { @Test - void testRunCore() { + void testRunCore() throws ConfigurationException{ Core core1=new Core(); core1.runCore(); From 4e2d400d0521ee18997a3e8428f0fb7ac5517b57 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Sun, 25 Jun 2023 10:46:16 +0200 Subject: [PATCH 27/71] Implemented Offer Placement in OfferProvisioner --- .../rwu/easydrop/core/OfferProvisioner.java | 77 +++++++++++++++---- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java index 27aa676..0209812 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java @@ -1,18 +1,49 @@ package de.rwu.easydrop.core; +import java.io.IOException; import java.util.List; +import de.rwu.easydrop.util.Config; import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.exception.DataWriterException; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.service.retriever.ProductRetriever; import de.rwu.easydrop.service.writer.OfferWriter; +import de.rwu.easydrop.api.client.AmazonSeller; +import de.rwu.easydrop.api.client.EbaySeller; +import de.rwu.easydrop.api.dto.ProductDTO; public class OfferProvisioner { - ProductRetriever productRetriever; + Config config; + ProductRetriever productRetriever; // ProductWriter AbstractProductPersistence db; OfferWriter offerWriter; + AmazonSeller amazonSeller; + EbaySeller ebaySeller; + + private void toSeller(Offer offer) throws IllegalArgumentException { + // TODO dataOrigin should use the webshop enum + if(offer.getSaleProduct().getDataOrigin().toLowerCase().equals("ebay")){ + this.ebaySeller.sellProduct(new ProductDTO( + offer.getSaleProduct().getProductId(), + "Amazon") + ); + + } else if (offer.getSaleProduct().getDataOrigin().toLowerCase().equals("Amazon")) { + this.amazonSeller.sellProduct(new ProductDTO( + offer.getSaleProduct().getProductId(), + "eBay") + ); + } + else { + throw new IllegalArgumentException("Unsupported target plattform"); + } + + } + + public OfferProvisioner(/*OfferWriter for database? */) { @@ -20,6 +51,15 @@ public class OfferProvisioner { //oder eine Konstante anlegen die das halten würde (besser skalierbar) //die Konstante in model anlegen (in Product, Konstante valid origins) this.offerWriter = new OfferWriter(); + this.config = Config.getInstance(); + this.amazonSeller = new AmazonSeller( + config.getProperty("AMAZON_API_URL"), + config.getProperty("AMAZON_API_KEY") + ); + this.ebaySeller = new EbaySeller( + config.getProperty("EBAY_API_URL"), + config.getProperty("EBAY_API_KEY") + ); } public void runProvisioner(List offersToProvision) { @@ -28,23 +68,28 @@ public class OfferProvisioner { * Stellt Angebote bei der Zielplattform ein * Schreibt eingestellte Angebote in die Datenbank */ - for(Offer newOffer: offersToProvision){ - // if(newOffer.getSourceProduct().getDataOrigin().equals("ebay")){ - // //put newOffer on amazon - // } - //else put newOffer on eBay - - // if successfully transmitted - // add to persistence - // "duplicate" the product with dataOrigin new platform and merchant = "me" - offerWriter.writeOfferToPersistence(newOffer); + try { + this.toSeller(newOffer); + // if successfully transmitted + // add to persistence + // "duplicate" the product with dataOrigin new platform and merchant = "me" + try { + offerWriter.writeOfferToPersistence(newOffer); + } + catch (Exception e){ + System.out.println("Could not write to persistence"); + } + } catch (IllegalArgumentException e) { + System.out.println( + "Offer could not be placed, " + + newOffer.getSaleProduct().getDataOrigin() + + " is not supported" + ); + } catch (DataWriterException e) { + System.out.println("could not transmit offer"); + } } - - - - } - } From c18f6a7fdcc2525b33f67e3e4a0adcbbeb7e7641 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Sun, 25 Jun 2023 10:47:29 +0200 Subject: [PATCH 28/71] Need DatabaseConnector on SQLite --- .../easydrop/api/client/AbstractDataSource.java | 14 ++++++++++++-- .../easydrop/data/connector/DatabaseConnector.java | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java index 4b58d66..5d410a8 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java @@ -72,13 +72,23 @@ public abstract class AbstractDataSource implements DataSource { + responseCode); } - String line; + + String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); - buildProductDTO(product, response.toString()); + // FIXME: Mock not complete. Data is missing + // FIXME response can be empty + String data; + if (response.toString().isEmpty()){ + data = "{}"; + } + else { + data = response.toString(); + } + buildProductDTO(product, data); } catch (IOException e) { throw new DataSourceException( "Couldn't fulfill " 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 { + +} From f715c76ace1724023551da7051ddbc41d2f276b2 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Mon, 26 Jun 2023 00:21:42 +0200 Subject: [PATCH 29/71] Integrated price function into offer identifier --- src/main/java/de/rwu/easydrop/core/Core.java | 5 +- .../de/rwu/easydrop/core/OfferIdentifier.java | 83 +++++++++---------- .../java/de/rwu/easydrop/core/CoreTest.java | 19 ++++- 3 files changed, 62 insertions(+), 45 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java index 82af9ef..d8fcd99 100644 --- a/src/main/java/de/rwu/easydrop/core/Core.java +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -5,6 +5,7 @@ import java.util.List; import javax.naming.ConfigurationException; import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.ProductCatalogue; public class Core { @@ -23,8 +24,8 @@ public class Core { } - public void runCore(){ - List identifiedOffers = ident.runIdentifier(); + public void runCore(List pCats){ + List identifiedOffers = ident.runIdentifier(pCats); provis.runProvisioner(identifiedOffers); List updatingOffers = review.runReviewer(); update.runUpdater(updatingOffers); diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java index 2093052..4f3ee39 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java +++ b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java @@ -1,61 +1,63 @@ package de.rwu.easydrop.core; -import de.rwu.easydrop.api.client.DataSourceFactory; -import de.rwu.easydrop.model.Offer; -import de.rwu.easydrop.model.Product; -import de.rwu.easydrop.model.ProductCatalogue; -import de.rwu.easydrop.service.retriever.OfferRetriever; -import de.rwu.easydrop.service.retriever.ProductRetriever; -import de.rwu.easydrop.util.Config; - import javax.naming.ConfigurationException; import java.util.List; import java.util.ArrayList; +import java.util.Date; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.ProductPair; +import de.rwu.easydrop.service.retriever.OfferRetriever; +import de.rwu.easydrop.service.processing.OrderManager; +import de.rwu.easydrop.exception.InvalidCatalogueException; public class OfferIdentifier { - + /** + * Logger for main process. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(OfferIdentifier.class); OfferRetriever offerRetriever; - ProductRetriever productRetriever; - public OfferIdentifier() throws ConfigurationException{ this.offerRetriever = new OfferRetriever(); - DataSourceFactory dataSourceFactory = new DataSourceFactory(Config.getInstance()); - this.productRetriever= new ProductRetriever(dataSourceFactory); } - public List runIdentifier() { - /* muss die Kataloge durchforsten nach vergleichbaren Produkten auf mehreren Händlerwebseiten (APIs) - * muss Preisermittler als Abhängigkeit für jeden Katalog aufrufen - * erhält dadurch Liste von Produkten, die auf verschiedenen Plattformen eingestellt werden können, gibt diese Liste zurück - * muss Datenbank nach Produktangeboten durchforsten, ob diese Produkte schon eingestellt wurden vom OfferProvisioner - * wenn ja, muss er die Liste um diese Angebote kürzen - * - */ + public List runIdentifier(List pCats) { + List identifiedOffers = new ArrayList<>(); + // Here we call the price function that decides if is feasible to dropship the product and if, at which + // margin + for (ProductCatalogue pCat : pCats) { + try{ + // Call price finder for all catalogue + ProductPair pair = OrderManager.getHighestMarginProducts(pCat); + Offer possibleOffer = new Offer(); + possibleOffer.setCheckDate(new Date()); + possibleOffer.setSourceProduct(pair.getProduct1()); + possibleOffer.setSaleProduct(pair.getProduct2()); + identifiedOffers.add(possibleOffer); + LOGGER.info( + "Identified offer " + + pair.getProduct1().getProductId() + + " -> " + + pair.getProduct2().getProductId() + ); + // Following fields will be set if offer confirmed + // creationDate, offerId - // Just an example here taken from demo.products-config.json; should be generic and configurable either via - // JSON or the database later. - ProductCatalogue catalogue = new ProductCatalogue("Gigabyte GeForce RTX 3060", "Very epic GPU"); - for(Product.webshop webshop: Product.webshop.values()) { - // The product retriever should know what API with which ProductID to query; high-level components should not know about such - // things. - Product product = this.productRetriever.getProductFromDataSource(webshop, catalogue.getProductName()); - if(product != null) { - catalogue.addProduct(product); + // Following fields will be set if offer needs update + // upDate + } + catch(InvalidCatalogueException e) { + // if no margin, getHighestMarginProducts will throw + System.out.print("product has no margin"); } } - List identifiedOffers = new ArrayList<>(); - // Here we would call the price function that decides if is feasible to dropship the product and if, at which - // margin - // Offer possibleOffer = GenerateOfferFromCatalogue(catalogue); - // if(possibleOffer != null) { - // identifiedOffers.add(possibleOffer); - // } - List newOffers = new ArrayList<>(); List existingOffers = offerRetriever.loadOffers(); for(Offer identifiedOffer: identifiedOffers) { @@ -70,9 +72,6 @@ public class OfferIdentifier { newOffers.add(identifiedOffer); } } - return newOffers; - } - - + } } diff --git a/src/test/java/de/rwu/easydrop/core/CoreTest.java b/src/test/java/de/rwu/easydrop/core/CoreTest.java index 5eb429c..e37170f 100644 --- a/src/test/java/de/rwu/easydrop/core/CoreTest.java +++ b/src/test/java/de/rwu/easydrop/core/CoreTest.java @@ -1,14 +1,31 @@ package de.rwu.easydrop.core; +import java.util.List; import javax.naming.ConfigurationException; import org.junit.jupiter.api.Test; +import de.rwu.easydrop.api.client.DataSourceFactory; + +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.retriever.CatalogueRetriever; +import de.rwu.easydrop.service.retriever.ProductRetriever; +import de.rwu.easydrop.util.Config; +import de.rwu.easydrop.util.ProductsConfig; + public class CoreTest { @Test void testRunCore() throws ConfigurationException{ + Config config = Config.getInstance(); + ProductsConfig pConfig = ProductsConfig.getInstance(); + DataSourceFactory dataSourceFactory = new DataSourceFactory(config); + ProductRetriever retriever = new ProductRetriever(dataSourceFactory); + CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); + + catRetriever.loadCatalogues(); + List pCats = catRetriever.getProductCatalogues(); Core core1=new Core(); - core1.runCore(); + core1.runCore(pCats); } } From 399951c43d89596fc2676bfcc730b417bf223cd8 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Mon, 26 Jun 2023 00:34:23 +0200 Subject: [PATCH 30/71] Integrated Core into Main --- src/main/java/de/rwu/easydrop/Main.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index 87a23a5..c8f4f1e 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -9,10 +9,10 @@ import org.slf4j.LoggerFactory; import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.core.Core; import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.data.connector.SQLiteConnector; import de.rwu.easydrop.model.ProductCatalogue; -import de.rwu.easydrop.service.processing.OrderManager; import de.rwu.easydrop.service.retriever.CatalogueRetriever; import de.rwu.easydrop.service.retriever.ProductRetriever; import de.rwu.easydrop.service.writer.CatalogueWriter; @@ -55,11 +55,7 @@ public final class Main { List pCats = catRetriever.getProductCatalogues(); catWriter.writeCatalogues(pCats); - for (ProductCatalogue pCat : pCats) { - String pCatStr = pCat.toString(); - LOGGER.info(pCatStr); - } - - OrderManager.createOrders(pCats); + Core core = new Core(); + core.runCore(pCats); } } From e3377c7b5924085f5ae05c21e7a78a70408d5cad Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Mon, 26 Jun 2023 10:03:03 +0200 Subject: [PATCH 31/71] Did Checkstyle improvements on OfferProv + Ident --- .../de/rwu/easydrop/core/OfferIdentifier.java | 57 +++++++---- .../rwu/easydrop/core/OfferProvisioner.java | 99 ++++++++++--------- 2 files changed, 90 insertions(+), 66 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java index 4f3ee39..b240d0f 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java +++ b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java @@ -20,58 +20,73 @@ public class OfferIdentifier { * Logger for main process. */ private static final Logger LOGGER = LoggerFactory.getLogger(OfferIdentifier.class); - - OfferRetriever offerRetriever; - + /** + * OfferRetriever gets the offer from persistence. + */ + private OfferRetriever offerRetriever; + /** + * OfferIdentifier identifies offers that are + * feasible to place on a target platform based on + * their margin. + * @throws ConfigurationException + */ public OfferIdentifier() throws ConfigurationException{ this.offerRetriever = new OfferRetriever(); } - public List runIdentifier(List pCats) { + /** + * runIdentifier calls the price function that decides + * if it is feasible to dropship products and if so, + * at which margin. + * @param pCats + * @return newOffers + */ + public List runIdentifier(final List pCats) { List identifiedOffers = new ArrayList<>(); - // Here we call the price function that decides if is feasible to dropship the product and if, at which - // margin for (ProductCatalogue pCat : pCats) { - try{ + try { // Call price finder for all catalogue - ProductPair pair = OrderManager.getHighestMarginProducts(pCat); + ProductPair pair = OrderManager.getHighestMarginProducts(pCat); Offer possibleOffer = new Offer(); possibleOffer.setCheckDate(new Date()); possibleOffer.setSourceProduct(pair.getProduct1()); possibleOffer.setSaleProduct(pair.getProduct2()); - identifiedOffers.add(possibleOffer); + identifiedOffers.add(possibleOffer); LOGGER.info( - "Identified offer " + - pair.getProduct1().getProductId() + - " -> " + + "Identified offer " + + + pair.getProduct1().getProductId() + + + " -> " + + pair.getProduct2().getProductId() - ); + ); // Following fields will be set if offer confirmed // creationDate, offerId - // Following fields will be set if offer needs update // upDate } - catch(InvalidCatalogueException e) { + catch (InvalidCatalogueException e) { // if no margin, getHighestMarginProducts will throw System.out.print("product has no margin"); } } - List newOffers = new ArrayList<>(); List existingOffers = offerRetriever.loadOffers(); - for(Offer identifiedOffer: identifiedOffers) { + for (Offer identifiedOffer: identifiedOffers) { boolean isNew = true; - for(Offer existingOffer: existingOffers) { - if(existingOffer.getSourceProduct().getProductId().equals(identifiedOffer.getSourceProduct().getProductId())) { + for (Offer existingOffer: existingOffers) { + if ( + existingOffer.getSourceProduct().getProductId(). + equals(identifiedOffer.getSourceProduct().getProductId())) { isNew = false; break; } } - if(isNew) { + if (isNew) { newOffers.add(identifiedOffer); } } return newOffers; - } + } } diff --git a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java index 0209812..bd7c063 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java @@ -1,6 +1,5 @@ package de.rwu.easydrop.core; -import java.io.IOException; import java.util.List; import de.rwu.easydrop.util.Config; @@ -14,61 +13,72 @@ import de.rwu.easydrop.api.client.EbaySeller; import de.rwu.easydrop.api.dto.ProductDTO; public class OfferProvisioner { - - Config config; - ProductRetriever productRetriever; + /** + * Config. + */ + private Config config; + /** + * Gets the products from persistence. + */ + private ProductRetriever productRetriever; // ProductWriter - AbstractProductPersistence db; - OfferWriter offerWriter; - AmazonSeller amazonSeller; - EbaySeller ebaySeller; + /** + * Is the product databank. + */ + private AbstractProductPersistence db; + /** + * Writes offers into persistence. + */ + private OfferWriter offerWriter; - private void toSeller(Offer offer) throws IllegalArgumentException { - // TODO dataOrigin should use the webshop enum - if(offer.getSaleProduct().getDataOrigin().toLowerCase().equals("ebay")){ - this.ebaySeller.sellProduct(new ProductDTO( - offer.getSaleProduct().getProductId(), - "Amazon") - ); + /** + * Is the API for selling products on Amazon. + */ + private AmazonSeller amazonSeller; + /** + * Is the API for selling products on Ebay. + */ + private EbaySeller ebaySeller; - } else if (offer.getSaleProduct().getDataOrigin().toLowerCase().equals("Amazon")) { - this.amazonSeller.sellProduct(new ProductDTO( + private void toSeller(final Offer offer) throws IllegalArgumentException { + // TODO dataOrigin should use the webshop enum + if (offer.getSaleProduct().getDataOrigin().toLowerCase().equals("ebay")) { + this.ebaySeller.sellProduct(new ProductDTO( offer.getSaleProduct().getProductId(), - "eBay") - ); - } - else { - throw new IllegalArgumentException("Unsupported target plattform"); - } + "Amazon")); + + } else if (offer.getSaleProduct().getDataOrigin().toLowerCase().equals("Amazon")) { + this.amazonSeller.sellProduct(new ProductDTO( + offer.getSaleProduct().getProductId(), + "eBay")); + } else { + throw new IllegalArgumentException("Unsupported target plattform"); + } } - + /** + * Is the class for placing orders on a target platform. + */ + public OfferProvisioner(/* OfferWriter for database? */) { - public OfferProvisioner(/*OfferWriter for database? */) { - - //in die Klasse hardcoden, "webshop" nicht verwenden - //oder eine Konstante anlegen die das halten würde (besser skalierbar) - //die Konstante in model anlegen (in Product, Konstante valid origins) this.offerWriter = new OfferWriter(); this.config = Config.getInstance(); this.amazonSeller = new AmazonSeller( - config.getProperty("AMAZON_API_URL"), - config.getProperty("AMAZON_API_KEY") - ); + config.getProperty("AMAZON_API_URL"), + config.getProperty("AMAZON_API_KEY")); this.ebaySeller = new EbaySeller( - config.getProperty("EBAY_API_URL"), - config.getProperty("EBAY_API_KEY") - ); + config.getProperty("EBAY_API_URL"), + config.getProperty("EBAY_API_KEY")); } - public void runProvisioner(List offersToProvision) { - /* - * Bekommt vom Identifier eine Liste mit Angeboten, die er einstellen soll auf Zielplattformen - * Stellt Angebote bei der Zielplattform ein - * Schreibt eingestellte Angebote in die Datenbank - */ - for(Offer newOffer: offersToProvision){ + /** + * Method for placing orders on a target platform. + * @param offersToProvision + */ + public final void runProvisioner(final List offersToProvision) { + + for (Offer newOffer: offersToProvision){ try { this.toSeller(newOffer); @@ -77,10 +87,9 @@ public class OfferProvisioner { // "duplicate" the product with dataOrigin new platform and merchant = "me" try { offerWriter.writeOfferToPersistence(newOffer); - } - catch (Exception e){ + } catch (Exception e) { System.out.println("Could not write to persistence"); - } + } } catch (IllegalArgumentException e) { System.out.println( "Offer could not be placed, " + From 6cd6b779a6a31965e56e1556ed11179fbd925932 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Mon, 26 Jun 2023 12:55:52 +0200 Subject: [PATCH 32/71] More checkstyle improvements on Offer components --- .../java/de/rwu/easydrop/model/Offer.java | 36 +++++++++---------- .../service/validation/OfferValidator.java | 4 +-- .../easydrop/service/writer/OfferWriter.java | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/model/Offer.java b/src/main/java/de/rwu/easydrop/model/Offer.java index 174e1d4..8fae3b7 100644 --- a/src/main/java/de/rwu/easydrop/model/Offer.java +++ b/src/main/java/de/rwu/easydrop/model/Offer.java @@ -13,35 +13,35 @@ import lombok.Data; @Data public class Offer { - /* - * The product that our software buys + /** + * The product that our software buys. */ - Product sourceProduct; + private Product sourceProduct; - /* - * The product that our software sells + /** + * The product that our software sells. */ - Product saleProduct; + private Product saleProduct; - /* - * Date of the creation of the offer + /** + * Date of the creation of the offer. * NOTE: Use Timestamp? https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html */ - Date creationDate; + private Date creationDate; - /* - * Date of last update of the offer on the destination website (API) + /** + * Date of last update of the offer on the destination website (API). */ - Date upDate; + private Date upDate; - /* - * Date of last check if offer is still valid + /** + * Date of last check if offer is still valid. */ - Date checkDate; + private Date checkDate; - /* - * ID of the offer + /** + * ID of the offer. */ - String offerId; + private String offerId; } diff --git a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java index ab6b26e..c3f2257 100644 --- a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java +++ b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java @@ -7,7 +7,7 @@ import de.rwu.easydrop.model.Offer; * * @since 0.2.0 */ -public class OfferValidator { +public final class OfferValidator { /** * Private constructor to prevent unwanted instantiation. * @@ -21,7 +21,7 @@ public class OfferValidator { * * @param offer the Offer */ - public static void validate(final Offer offer) {} + public static void validate(final Offer offer) { } } diff --git a/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java b/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java index 8933d55..6ba6a4c 100644 --- a/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java +++ b/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java @@ -22,7 +22,7 @@ public class OfferWriter { /** * Validates and saves product to persistence. * - * @param product + * @param offer */ public void writeOfferToPersistence(final Offer offer) { OfferValidator.validate(offer); From 37e4970fd6c6046a8fad3c9f54953fd39d84dfc1 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Mon, 26 Jun 2023 15:33:51 +0200 Subject: [PATCH 33/71] Checkstyle improvements Product + OfferProvis --- .../de/rwu/easydrop/core/OfferProvisioner.java | 2 +- .../java/de/rwu/easydrop/model/Product.java | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java index bd7c063..457256a 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java @@ -47,7 +47,7 @@ public class OfferProvisioner { offer.getSaleProduct().getProductId(), "Amazon")); - } else if (offer.getSaleProduct().getDataOrigin().toLowerCase().equals("Amazon")) { + } else if (offer.getSaleProduct().getDataOrigin().toLowerCase().equals("amazon")) { this.amazonSeller.sellProduct(new ProductDTO( offer.getSaleProduct().getProductId(), "eBay")); diff --git a/src/main/java/de/rwu/easydrop/model/Product.java b/src/main/java/de/rwu/easydrop/model/Product.java index c8b70ff..d484405 100644 --- a/src/main/java/de/rwu/easydrop/model/Product.java +++ b/src/main/java/de/rwu/easydrop/model/Product.java @@ -10,13 +10,19 @@ import lombok.Data; */ @Data public class Product { - /* - * Constants for data source/destination platforms + /** + * Constants for data source/destination platforms. */ - public enum webshop{ - AMAZON, EBAY - } - + public enum Webshop { + /** + * Amazon. + */ + AMAZON, + /**Ebay. + */ + EBAY + } + /** * Data source platform, like "Amazon". */ From da7c4e99794607fa3d9ce6476406a0844745a0b5 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Mon, 26 Jun 2023 21:39:43 +0200 Subject: [PATCH 34/71] added method checkoffer --- .../de/rwu/easydrop/core/OfferReviewer.java | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java index f410f41..fb5f962 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java +++ b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java @@ -1,23 +1,80 @@ package de.rwu.easydrop.core; import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Product.webshop; + import java.util.List; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; import java.util.ArrayList; public class OfferReviewer { - public OfferReviewer(/*OfferReader/retriever for database? */){ + /** + * OfferReviewer. + * @return list of all items that need to be changed + */ + public List checkOffers(/*OfferReader/retriever for database? */) { - } + Connection connection = null; + Statement statement = null; + ResultSet resultSet = null; + List changedProducts = new ArrayList<>(); - public List runReviewer() { - /* - * Liest eingestellte Angebote in der Datenbank - * Prüft Zielplattformen der SourceProducts, ob diese noch verfügbar sind (Issue#12) bzw. ob sie sich im Preis geändert haben - * Gibt Liste zurück von Offers, die geändert werden müssen, wird übergeben an OfferUpdater mit availability true oder false - */ + try { + // Establish the database connection + connection = DriverManager.getConnection("jdbc:sqlite:persistence.db"); + + // Create a SQL statement + statement = connection.createStatement(); + + // Execute the query to retrieve the entries + String query = "SELECT dataOrigin, productId, currentPrice FROM table"; + resultSet = statement.executeQuery(query); + + // Process the retrieved entries + while (resultSet.next()) { + String webshop = resultSet.getString("webshop"); + String dataOrigin = resultSet.getString("data_origin"); + String productId = resultSet.getString("productId"); + double currentPrice = resultSet.getDouble("currentPrice"); + String merchant = resultSet.getString("merchant"); + double deliveryPrice = resultSet.getDouble("delivery_price"); + boolean available = resultSet.getBoolean("available"); + + // Call the API to get the current price + double apiPrice = getPriceFromAPI(productId); + + // Compare the prices + if (currentPrice != apiPrice) { + // Price has changed, create a Product object and add it to the changedProducts list + Product product = new Product(webshop, dataOrigin, productId, currentPrice, + merchant, deliveryPrice, available); + changedProducts.add(product); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } finally { + // Close the resources (resultSet, statement, connection) in a finally block + try { + if (resultSet != null) { + resultSet.close(); + } + if (statement != null) { + statement.close(); + } + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } return new ArrayList(); - } - } From 7a19a38aa19b160e530a8734a06b5ebd93c88ae3 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 00:14:02 +0200 Subject: [PATCH 35/71] fixed some issues in method checkOffers --- .../de/rwu/easydrop/core/OfferReviewer.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java index fb5f962..5b698bf 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java +++ b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java @@ -11,19 +11,20 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Date; public class OfferReviewer { /** - * OfferReviewer. + * Check all Offers and compare them with the API. * @return list of all items that need to be changed */ - public List checkOffers(/*OfferReader/retriever for database? */) { + public List checkOffer(/*OfferReader/retriever for database? */) { Connection connection = null; Statement statement = null; ResultSet resultSet = null; - List changedProducts = new ArrayList<>(); + List changedOffers = new ArrayList<>(); try { // Establish the database connection @@ -33,28 +34,27 @@ public class OfferReviewer { statement = connection.createStatement(); // Execute the query to retrieve the entries - String query = "SELECT dataOrigin, productId, currentPrice FROM table"; + String query = "SELECT sourceProduct, saleProduct, creationDate, upDate, " + + "checkDate, offerId FROM table"; resultSet = statement.executeQuery(query); // Process the retrieved entries while (resultSet.next()) { - String webshop = resultSet.getString("webshop"); - String dataOrigin = resultSet.getString("data_origin"); - String productId = resultSet.getString("productId"); - double currentPrice = resultSet.getDouble("currentPrice"); - String merchant = resultSet.getString("merchant"); - double deliveryPrice = resultSet.getDouble("delivery_price"); - boolean available = resultSet.getBoolean("available"); + Product sourceProduct = (Product) resultSet.getObject("sourceProduct"); + Product saleProduct = (Product) resultSet.getObject("saleProduct"); + java.sql.Date creationDate = resultSet.getDate("creationDate"); + Date updateDate = resultSet.getDate("upDate"); + Date checkDate = resultSet.getDate("checkDate"); + String offerId = resultSet.getString("offerId"); // Call the API to get the current price - double apiPrice = getPriceFromAPI(productId); + double apiPrice = getPriceFromAPI(sourceProduct); // Compare the prices - if (currentPrice != apiPrice) { - // Price has changed, create a Product object and add it to the changedProducts list - Product product = new Product(webshop, dataOrigin, productId, currentPrice, - merchant, deliveryPrice, available); - changedProducts.add(product); + if (saleProduct.getCurrentPrice() != apiPrice) { + // Price has changed, create a Product object and add it to the changedProducts list + Offer offer = new Offer(); + changedOffers.add(offer); } } } catch (SQLException e) { From d0b6178d26edc83b428d747e32b4f208419cd5fd Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 00:14:41 +0200 Subject: [PATCH 36/71] deleted an unused import --- src/main/java/de/rwu/easydrop/core/OfferReviewer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java index 5b698bf..ba9e3d4 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java +++ b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java @@ -2,7 +2,6 @@ package de.rwu.easydrop.core; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.Product; -import de.rwu.easydrop.model.Product.webshop; import java.util.List; import java.sql.Connection; From 22ee3f3d347ee79a8531754c47012af87cdb97e8 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Tue, 27 Jun 2023 00:45:41 +0200 Subject: [PATCH 37/71] Added webshops enum --- src/main/java/de/rwu/easydrop/model/Webshop.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/de/rwu/easydrop/model/Webshop.java diff --git a/src/main/java/de/rwu/easydrop/model/Webshop.java b/src/main/java/de/rwu/easydrop/model/Webshop.java new file mode 100644 index 0000000..b653b39 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/Webshop.java @@ -0,0 +1,15 @@ +package de.rwu.easydrop.model; + +/** + * Constants for data source/destination platforms. + */ +public enum Webshop { + /** + * Amazon. + */ + Amazon, + /** + * eBay. + */ + eBay +} From 1e5ee7259496af94464e9d406c81782a722608e1 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 01:48:10 +0200 Subject: [PATCH 38/71] added offerupdater --- .../de/rwu/easydrop/core/OfferReviewer.java | 4 +- .../de/rwu/easydrop/core/OfferUpdater.java | 105 ++++++++++++++++-- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java index ba9e3d4..e394093 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java +++ b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java @@ -59,7 +59,6 @@ public class OfferReviewer { } catch (SQLException e) { e.printStackTrace(); } finally { - // Close the resources (resultSet, statement, connection) in a finally block try { if (resultSet != null) { resultSet.close(); @@ -76,4 +75,7 @@ public class OfferReviewer { } return new ArrayList(); } + + + } diff --git a/src/main/java/de/rwu/easydrop/core/OfferUpdater.java b/src/main/java/de/rwu/easydrop/core/OfferUpdater.java index 33091cb..27d08a9 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferUpdater.java +++ b/src/main/java/de/rwu/easydrop/core/OfferUpdater.java @@ -1,21 +1,110 @@ package de.rwu.easydrop.core; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.List; import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; + public class OfferUpdater { + /** + * a. + */ public OfferUpdater() { + OfferReviewer offerReviewer = new OfferReviewer(); + List changedOffers = offerReviewer.checkOffer(); } - - public void runUpdater(List offersToUpdate) { - /* - * Bekommt vom OfferReviewer eine Liste mit zu ändernden Offers übergeben. - * Bei availability=false löscht der Updater das Angebot aus der Datenbank und von der Zielplattform. - * Der Updater ändert die geänderten Parameter in der Liste in der Datenbank und in der Zielplattform ab. - * Er ändert das upDate in der Datenbank nach der Änderung. - */ + /** + * A. + * @param offersToUpdate + */ + public void runUpdater(final List offersToUpdate) { + Connection connection = null; + PreparedStatement deleteStatement = null; + PreparedStatement insertStatement = null; + + try { + // Establish the database connection + connection = DriverManager.getConnection("jdbc:sqlite:persistence.db"); + + // Disable auto-commit to perform a transaction + connection.setAutoCommit(false); + + // Prepare the DELETE statement to remove the existing entries + String deleteQuery = "DELETE FROM your_table WHERE product_id = ?"; + deleteStatement = connection.prepareStatement(deleteQuery); + + // Prepare the INSERT statement to add the new entries + String insertQuery = "INSERT INTO your_table (product_id, product_name, price)" + + "VALUES (?, ?, ?)"; + insertStatement = connection.prepareStatement(insertQuery); + + // Retrieve the existing entries from the database + List existingProducts = retrieveExistingProducts(connection); + + // Delete the existing entries that are not present in the changedProducts list + for (Product existingProduct : existingProducts) { + if (!changedOffers.(existingProduct)) { + deleteStatement.setString(1, existingProduct.getProductId()); + deleteStatement.executeUpdate(); + } + } + + // Insert the new entries or update the existing entries + for (Product changedOffers : offersToUpdate) { + if (existingProducts.contains(changedOffers)) { + // Update the existing entry with the new data + // You need to modify the update query and statement based on your requirements + // Here's an example of updating the price of an existing entry + String updateQuery = "UPDATE table SET currentPrice = ? WHERE offerId = ?"; + PreparedStatement updateStatement = connection.prepareStatement(updateQuery); + updateStatement.setDouble(1, changedOffers.getCurrentPrice()); + updateStatement.setString(2, changedOffers.getProductId()); + updateStatement.executeUpdate(); + updateStatement.close(); + } else { + // Insert the new entry + insertStatement.setString(1, changedOffers.getProductId()); + insertStatement.setString(2, changedOffers.getMerchant()); + insertStatement.setDouble(3, changedOffers.getCurrentPrice()); + insertStatement.executeUpdate(); + } + } + + // Commit the transaction + connection.commit(); + } catch (SQLException e) { + // Rollback the transaction in case of an exception + if (connection != null) { + try { + connection.rollback(); + } catch (SQLException rollbackException) { + rollbackException.printStackTrace(); + } + } + e.printStackTrace(); + } finally { + // Close the resources (deleteStatement, insertStatement, connection) in a finally block + try { + if (deleteStatement != null) { + deleteStatement.close(); + } + if (insertStatement != null) { + insertStatement.close(); + } + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } } } +} + From 463932f8e9db11086944f5e99c96f53241f4d4cb Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Tue, 27 Jun 2023 02:56:28 +0200 Subject: [PATCH 39/71] Webshops refactoring + checkstyle --- .../api/client/AbstractDataSource.java | 17 +++---- .../api/client/AbstractDataWriter.java | 5 +- .../api/client/AmazonProductDataSource.java | 5 +- .../easydrop/api/client/AmazonPurchaser.java | 6 ++- .../rwu/easydrop/api/client/AmazonSeller.java | 6 ++- .../api/client/EbayItemDataSource.java | 5 +- .../easydrop/api/client/EbayPurchaser.java | 6 ++- .../rwu/easydrop/api/client/EbaySeller.java | 6 ++- .../de/rwu/easydrop/api/dto/OfferDTO.java | 33 ++++++------- .../de/rwu/easydrop/api/dto/ProductDTO.java | 7 +-- src/main/java/de/rwu/easydrop/core/Core.java | 39 ++++++++------- .../de/rwu/easydrop/core/OfferIdentifier.java | 35 +++++--------- .../rwu/easydrop/core/OfferProvisioner.java | 47 +++++++------------ .../de/rwu/easydrop/core/package-info.java | 6 +++ .../connector/AbstractOfferPersistence.java | 15 +++--- .../data/connector/SQLiteConnector.java | 18 ++++--- .../java/de/rwu/easydrop/model/Product.java | 15 +----- .../java/de/rwu/easydrop/model/Webshop.java | 22 +++++++-- .../easydrop/service/mapping/OfferMapper.java | 3 +- .../service/retriever/CatalogueRetriever.java | 12 +---- .../service/retriever/OfferRetriever.java | 9 ---- .../service/retriever/ProductRetriever.java | 33 ++++++------- .../service/validation/ProductValidator.java | 21 --------- .../de/rwu/easydrop/util/ProductsConfig.java | 5 +- 24 files changed, 168 insertions(+), 208 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/core/package-info.java diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java index 5d410a8..bfdad1a 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java @@ -9,6 +9,7 @@ import java.net.URL; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.DataSourceException; +import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.util.FormattingUtil; /** @@ -23,7 +24,7 @@ public abstract class AbstractDataSource implements DataSource { * * @return Data source name */ - protected abstract String getDataOrigin(); + protected abstract Webshop getDataOrigin(); /** * Returns the data source's API key. @@ -48,7 +49,7 @@ public abstract class AbstractDataSource implements DataSource { public ProductDTO getProductDTOById(final String productIdentifier) throws IllegalArgumentException { StringBuilder response = new StringBuilder(); - String dataOrigin = getDataOrigin(); + Webshop dataOrigin = getDataOrigin(); String apiKey = getApiKey(); ProductDTO product = new ProductDTO(productIdentifier, dataOrigin); @@ -72,20 +73,16 @@ public abstract class AbstractDataSource implements DataSource { + responseCode); } - - String line; + String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); - // FIXME: Mock not complete. Data is missing - // FIXME response can be empty String data; - if (response.toString().isEmpty()){ - data = "{}"; - } - else { + if (response.toString().isEmpty()) { + data = "{}"; + } else { data = response.toString(); } buildProductDTO(product, data); diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java b/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java index fd7a615..6357230 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java @@ -7,6 +7,7 @@ import java.net.URL; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.DataWriterException; +import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.util.FormattingUtil; /** @@ -28,7 +29,7 @@ public abstract class AbstractDataWriter { * * @return Data target API name */ - protected abstract String getDataTarget(); + protected abstract Webshop getDataTarget(); /** * Creates an URL object to connect to the API with. @@ -47,7 +48,7 @@ public abstract class AbstractDataWriter { */ protected void sendPutRequest(final ProductDTO dto, final String apiType) { String apiKey = getApiKey(); - String dataTarget = getDataTarget(); + Webshop dataTarget = getDataTarget(); if (!dataTarget.equals(dto.getDataOrigin())) { throw new DataWriterException( diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java index 83b758d..404398c 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java @@ -8,6 +8,7 @@ import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.ReadContext; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Webshop; /** * Interface to an Amazon data source. @@ -18,7 +19,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { /** * Name of this data source. */ - private static final String DATA_ORIGIN = "Amazon"; + private static final Webshop DATA_ORIGIN = Webshop.Amazon; /** * Base URL to the Amazon data source. */ @@ -82,7 +83,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { } @Override - protected String getDataOrigin() { + protected Webshop getDataOrigin() { return DATA_ORIGIN; } diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java index 283ed61..d1dc75c 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java @@ -3,6 +3,8 @@ package de.rwu.easydrop.api.client; import java.net.MalformedURLException; import java.net.URL; +import de.rwu.easydrop.model.Webshop; + /** * Sends a buy request to the Amazon API. * @@ -12,7 +14,7 @@ public final class AmazonPurchaser extends AbstractPurchaser { /** * Name of this data source. */ - private static final String DATA_TARGET = "Amazon"; + private static final Webshop DATA_TARGET = Webshop.Amazon; /** * Base URL to the Amazon Purchase API. */ @@ -61,7 +63,7 @@ public final class AmazonPurchaser extends AbstractPurchaser { } @Override - protected String getDataTarget() { + protected Webshop getDataTarget() { return DATA_TARGET; } } diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java b/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java index 8a47d51..7b33138 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java @@ -3,6 +3,8 @@ package de.rwu.easydrop.api.client; import java.net.MalformedURLException; import java.net.URL; +import de.rwu.easydrop.model.Webshop; + /** * Sends a sell request to the Amazon API. * @@ -12,7 +14,7 @@ public final class AmazonSeller extends AbstractSeller { /** * Name of this data source. */ - private static final String DATA_TARGET = "Amazon"; + private static final Webshop DATA_TARGET = Webshop.Amazon; /** * Base URL to the Amazon Purchase API. */ @@ -61,7 +63,7 @@ public final class AmazonSeller extends AbstractSeller { } @Override - protected String getDataTarget() { + protected Webshop getDataTarget() { return DATA_TARGET; } } diff --git a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java index 171b8b9..5e62394 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java @@ -8,6 +8,7 @@ import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.ReadContext; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Webshop; /** * Interface to an eBay data source. @@ -18,7 +19,7 @@ public final class EbayItemDataSource extends AbstractDataSource { /** * Name of this data source. */ - private static final String DATA_ORIGIN = "eBay"; + private static final Webshop DATA_ORIGIN = Webshop.eBay; /** * Base URL to the eBay data source. */ @@ -77,7 +78,7 @@ public final class EbayItemDataSource extends AbstractDataSource { } @Override - protected String getDataOrigin() { + protected Webshop getDataOrigin() { return DATA_ORIGIN; } diff --git a/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java index d9f69f8..ebaf121 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java @@ -3,6 +3,8 @@ package de.rwu.easydrop.api.client; import java.net.MalformedURLException; import java.net.URL; +import de.rwu.easydrop.model.Webshop; + /** * Sends a buy request to the eBay API. * @@ -12,7 +14,7 @@ public final class EbayPurchaser extends AbstractPurchaser { /** * Name of this data source. */ - private static final String DATA_TARGET = "eBay"; + private static final Webshop DATA_TARGET = Webshop.eBay; /** * Base URL to the eBay Purchase API. */ @@ -46,7 +48,7 @@ public final class EbayPurchaser extends AbstractPurchaser { } @Override - protected String getDataTarget() { + protected Webshop getDataTarget() { return DATA_TARGET; } } diff --git a/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java b/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java index 805ee5d..55835ec 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java @@ -3,6 +3,8 @@ package de.rwu.easydrop.api.client; import java.net.MalformedURLException; import java.net.URL; +import de.rwu.easydrop.model.Webshop; + /** * Sends a sell request to the eBay API. * @@ -12,7 +14,7 @@ public final class EbaySeller extends AbstractSeller { /** * Name of this data source. */ - private static final String DATA_TARGET = "eBay"; + private static final Webshop DATA_TARGET = Webshop.eBay; /** * Base URL to the eBay Purchase API. */ @@ -46,7 +48,7 @@ public final class EbaySeller extends AbstractSeller { } @Override - protected String getDataTarget() { + protected Webshop getDataTarget() { return DATA_TARGET; } } diff --git a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java index ee7d5f4..c2d680f 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java @@ -14,36 +14,37 @@ public class OfferDTO { */ private String offerId; - /* - * The product that our software buys + /** + * The product that our software buys. */ - Product sourceProduct; + private Product sourceProduct; - /* - * The product that our software sells + /** + * The product that our software sells. */ - Product saleProduct; + private Product saleProduct; - /* + /** * Date of the creation of the offer - * NOTE: Use Timestamp? https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html + * NOTE: Use Timestamp? + * https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html */ - Date creationDate; + private Date creationDate; - /* - * Date of last update of the offer on the destination website (API) + /** + * Date of last update of the offer on the destination website (API). */ - Date upDate; + private Date upDate; - /* - * Date of last check if offer is still valid + /** + * Date of last check if offer is still valid. */ - Date checkDate; + private Date checkDate; /** * Creates OfferDTO instance. * - * @param newOfferId Internal Offer identifier + * @param newOfferId Internal Offer identifier */ public OfferDTO(final String newOfferId) { this.offerId = newOfferId; diff --git a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java index f6beb85..7d7ea70 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java @@ -1,5 +1,6 @@ package de.rwu.easydrop.api.dto; +import de.rwu.easydrop.model.Webshop; import lombok.Data; /** @@ -10,9 +11,9 @@ import lombok.Data; @Data public class ProductDTO { /** - * Data source platform, like "Amazon". + * Data source platform, like Amazon. */ - private String dataOrigin; + private Webshop dataOrigin; /** * Platform internal product identifier. @@ -45,7 +46,7 @@ public class ProductDTO { * @param newProductId Internal Product indetifier * @param newDataOrigin Data Origin */ - public ProductDTO(final String newProductId, final String newDataOrigin) { + public ProductDTO(final String newProductId, final Webshop newDataOrigin) { this.productId = newProductId; this.dataOrigin = newDataOrigin; } diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java index d8fcd99..ce7e63b 100644 --- a/src/main/java/de/rwu/easydrop/core/Core.java +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -7,32 +7,39 @@ import javax.naming.ConfigurationException; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.ProductCatalogue; +/** + * The application core. + * + * @since 0.3.0 + */ public class Core { - + /** + * Offer identifier. + */ private OfferIdentifier ident; + /** + * Offer provisioner. + */ private OfferProvisioner provis; - private OfferReviewer review; - private OfferUpdater update; - - public Core() throws ConfigurationException{ + /** + * Constructor. + * + * @throws ConfigurationException + */ + public Core() throws ConfigurationException { this.ident = new OfferIdentifier(); this.provis = new OfferProvisioner(); - this.review = new OfferReviewer(); - this.update = new OfferUpdater(); } - - public void runCore(List pCats){ + /** + * Runs the core. + * + * @param pCats + */ + public void runCore(final List pCats) { List identifiedOffers = ident.runIdentifier(pCats); provis.runProvisioner(identifiedOffers); - List updatingOffers = review.runReviewer(); - update.runUpdater(updatingOffers); - - - } - - } diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java index b240d0f..99d4b23 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java +++ b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java @@ -14,7 +14,6 @@ import de.rwu.easydrop.service.retriever.OfferRetriever; import de.rwu.easydrop.service.processing.OrderManager; import de.rwu.easydrop.exception.InvalidCatalogueException; - public class OfferIdentifier { /** * Logger for main process. @@ -24,13 +23,15 @@ public class OfferIdentifier { * OfferRetriever gets the offer from persistence. */ private OfferRetriever offerRetriever; + /** * OfferIdentifier identifies offers that are * feasible to place on a target platform based on * their margin. + * * @throws ConfigurationException */ - public OfferIdentifier() throws ConfigurationException{ + public OfferIdentifier() throws ConfigurationException { this.offerRetriever = new OfferRetriever(); } @@ -38,6 +39,7 @@ public class OfferIdentifier { * runIdentifier calls the price function that decides * if it is feasible to dropship products and if so, * at which margin. + * * @param pCats * @return newOffers */ @@ -53,36 +55,25 @@ public class OfferIdentifier { possibleOffer.setSaleProduct(pair.getProduct2()); identifiedOffers.add(possibleOffer); LOGGER.info( - "Identified offer " - + - pair.getProduct1().getProductId() - + - " -> " - + - pair.getProduct2().getProductId() - ); + "Identified offer " + + + pair.getProduct1().getProductId() + + + " -> " + + + pair.getProduct2().getProductId()); // Following fields will be set if offer confirmed // creationDate, offerId // Following fields will be set if offer needs update // upDate - } - catch (InvalidCatalogueException e) { + } catch (InvalidCatalogueException e) { // if no margin, getHighestMarginProducts will throw System.out.print("product has no margin"); } } List newOffers = new ArrayList<>(); - List existingOffers = offerRetriever.loadOffers(); - for (Offer identifiedOffer: identifiedOffers) { + for (Offer identifiedOffer : identifiedOffers) { boolean isNew = true; - for (Offer existingOffer: existingOffers) { - if ( - existingOffer.getSourceProduct().getProductId(). - equals(identifiedOffer.getSourceProduct().getProductId())) { - isNew = false; - break; - } - } if (isNew) { newOffers.add(identifiedOffer); } diff --git a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java index 457256a..0f318bd 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java @@ -2,33 +2,23 @@ package de.rwu.easydrop.core; import java.util.List; -import de.rwu.easydrop.util.Config; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; -import de.rwu.easydrop.exception.DataWriterException; -import de.rwu.easydrop.model.Offer; -import de.rwu.easydrop.service.retriever.ProductRetriever; -import de.rwu.easydrop.service.writer.OfferWriter; import de.rwu.easydrop.api.client.AmazonSeller; import de.rwu.easydrop.api.client.EbaySeller; -import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.DataWriterException; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.service.mapping.ProductMapper; +import de.rwu.easydrop.service.writer.OfferWriter; +import de.rwu.easydrop.util.Config; public class OfferProvisioner { /** * Config. */ private Config config; - /** - * Gets the products from persistence. - */ - private ProductRetriever productRetriever; - // ProductWriter - /** - * Is the product databank. - */ - private AbstractProductPersistence db; /** * Writes offers into persistence. - */ + */ private OfferWriter offerWriter; /** @@ -41,16 +31,11 @@ public class OfferProvisioner { private EbaySeller ebaySeller; private void toSeller(final Offer offer) throws IllegalArgumentException { - // TODO dataOrigin should use the webshop enum - if (offer.getSaleProduct().getDataOrigin().toLowerCase().equals("ebay")) { - this.ebaySeller.sellProduct(new ProductDTO( - offer.getSaleProduct().getProductId(), - "Amazon")); + if (offer.getSaleProduct().getDataOrigin() == Webshop.eBay) { + this.ebaySeller.sellProduct(ProductMapper.mapProductToDTO(offer.getSaleProduct())); - } else if (offer.getSaleProduct().getDataOrigin().toLowerCase().equals("amazon")) { - this.amazonSeller.sellProduct(new ProductDTO( - offer.getSaleProduct().getProductId(), - "eBay")); + } else if (offer.getSaleProduct().getDataOrigin().equals(Webshop.Amazon)) { + this.amazonSeller.sellProduct(ProductMapper.mapProductToDTO(offer.getSaleProduct())); } else { throw new IllegalArgumentException("Unsupported target plattform"); } @@ -74,11 +59,12 @@ public class OfferProvisioner { /** * Method for placing orders on a target platform. + * * @param offersToProvision */ public final void runProvisioner(final List offersToProvision) { - for (Offer newOffer: offersToProvision){ + for (Offer newOffer : offersToProvision) { try { this.toSeller(newOffer); @@ -92,10 +78,9 @@ public class OfferProvisioner { } } catch (IllegalArgumentException e) { System.out.println( - "Offer could not be placed, " + - newOffer.getSaleProduct().getDataOrigin() + - " is not supported" - ); + "Offer could not be placed, " + + newOffer.getSaleProduct().getDataOrigin() + + " is not supported"); } catch (DataWriterException e) { System.out.println("could not transmit offer"); } diff --git a/src/main/java/de/rwu/easydrop/core/package-info.java b/src/main/java/de/rwu/easydrop/core/package-info.java new file mode 100644 index 0000000..c0963c1 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/core/package-info.java @@ -0,0 +1,6 @@ +/** + * EasyDrop's core. + * + * @since 0.3.0 + */ +package de.rwu.easydrop.core; diff --git a/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java b/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java index 4562a6d..5f4bbeb 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java +++ b/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java @@ -2,12 +2,7 @@ package de.rwu.easydrop.data.connector; import de.rwu.easydrop.api.dto.OfferDTO; -public abstract class AbstractOfferPersistence { - /** - * Data origin. - */ - public static final String DATA_ORIGIN = "Persistence"; - +public abstract class AbstractOfferPersistence { /** * Writes a ProductDTO to persistence. * @@ -17,12 +12,14 @@ public abstract class AbstractOfferPersistence { /** * Gets a OfferDTO from persistence. + * + * @param offerId + * @return Offer data transfer object */ - //@Override - public abstract OfferDTO getOfferDTOById(String OfferId); + public abstract OfferDTO getOfferDTOById(String offerId); /** * Deletes all data from persistence. */ - public abstract void clearData(); + public abstract void clearData(); } diff --git a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java index 6947c5d..43bb4fe 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -11,6 +11,7 @@ import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.PersistenceException; +import de.rwu.easydrop.model.Webshop; /** * Allows connecting to a SQLite Database. @@ -18,11 +19,6 @@ import de.rwu.easydrop.exception.PersistenceException; * @since 0.2.0 */ public final class SQLiteConnector extends AbstractProductPersistence { - /** - * Data origin. - */ - private static final String DATA_ORIGIN = "SQLite"; - /** * SQLite Database. */ @@ -95,7 +91,7 @@ public final class SQLiteConnector extends AbstractProductPersistence { PreparedStatement statement = connection.prepareStatement(query)) { int index = 0; - statement.setString(++index, dto.getDataOrigin()); + statement.setString(++index, dto.getDataOrigin().toString()); statement.setString(++index, dto.getProductId()); statement.setDouble(++index, dto.getCurrentPrice()); statement.setString(++index, dto.getMerchant()); @@ -120,8 +116,9 @@ public final class SQLiteConnector extends AbstractProductPersistence { try (ResultSet resultSet = statement.executeQuery()) { if (resultSet.next()) { - dto = new ProductDTO(resultSet.getString("productId"), - resultSet.getString("dataOrigin")); + Webshop newShop = Webshop.fromString(resultSet.getString("dataOrigin")); + + dto = new ProductDTO(resultSet.getString("productId"), newShop); dto.setCurrentPrice(resultSet.getDouble("currentPrice")); dto.setMerchant(resultSet.getString("merchant")); dto.setDeliveryPrice(resultSet.getDouble("deliveryPrice")); @@ -149,8 +146,9 @@ public final class SQLiteConnector extends AbstractProductPersistence { } @Override - protected String getDataOrigin() { - return DATA_ORIGIN; + protected Webshop getDataOrigin() { + throw new UnsupportedOperationException( + this.getClass().getName() + " doesn't support getDataOrigin"); } @Override diff --git a/src/main/java/de/rwu/easydrop/model/Product.java b/src/main/java/de/rwu/easydrop/model/Product.java index d484405..7e84311 100644 --- a/src/main/java/de/rwu/easydrop/model/Product.java +++ b/src/main/java/de/rwu/easydrop/model/Product.java @@ -10,23 +10,10 @@ import lombok.Data; */ @Data public class Product { - /** - * Constants for data source/destination platforms. - */ - public enum Webshop { - /** - * Amazon. - */ - AMAZON, - /**Ebay. - */ - EBAY - } - /** * Data source platform, like "Amazon". */ - private String dataOrigin; + private Webshop dataOrigin; /** * Platform internal product identifier. diff --git a/src/main/java/de/rwu/easydrop/model/Webshop.java b/src/main/java/de/rwu/easydrop/model/Webshop.java index b653b39..c4852b6 100644 --- a/src/main/java/de/rwu/easydrop/model/Webshop.java +++ b/src/main/java/de/rwu/easydrop/model/Webshop.java @@ -5,11 +5,27 @@ package de.rwu.easydrop.model; */ public enum Webshop { /** - * Amazon. + * Amazon Product API. */ Amazon, /** - * eBay. + * eBay Item API. */ - eBay + eBay; + + /** + * Attempts to derive a webshop value from a string. + * + * @param str String + * @return webshop + * @throws IllegalArgumentException + */ + public static Webshop fromString(final String str) { + for (Webshop shop : Webshop.values()) { + if (shop.toString().equalsIgnoreCase(str)) { + return shop; + } + } + throw new IllegalArgumentException(String.format("No webshop called {} found", str)); + } } diff --git a/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java index f6aeb73..a83a148 100644 --- a/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java +++ b/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java @@ -11,7 +11,6 @@ import de.rwu.easydrop.model.Offer; * * @see Offer * @see OfferDTO - * @see OfferDTO */ public final class OfferMapper { @@ -39,7 +38,7 @@ public final class OfferMapper { /** * Creates an OfferDTO object from a corresponding offer. * - * @param Offer offer + * @param offer * @return OfferDTO */ public static OfferDTO mapOfferToDTO(final Offer offer) { diff --git a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java index 64bb8c0..c299412 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java @@ -5,7 +5,6 @@ import java.util.List; import javax.naming.ConfigurationException; -import de.rwu.easydrop.exception.InvalidProductException; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.util.ProductsConfig; @@ -58,16 +57,9 @@ public class CatalogueRetriever { for (Product product : pCat.getProducts()) { Product newProduct = new Product(); - newProduct.setDataOrigin(product.getDataOrigin()); - newProduct.setProductId(product.getProductId()); - if (newProduct.getDataOrigin().equals("Amazon")) { - newProduct = productRetriever.getProductFromAmazon(product.getProductId()); - } else if (newProduct.getDataOrigin().equals("eBay")) { - newProduct = productRetriever.getProductFromEbay(product.getProductId()); - } else { - throw new InvalidProductException("Product data origin is invalid"); - } + newProduct = productRetriever.getProductFromWebshop(product.getDataOrigin(), + product.getProductId()); newProductCatalogue.addProduct(newProduct); } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java index 3571b7d..5488b29 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java @@ -1,14 +1,5 @@ package de.rwu.easydrop.service.retriever; -import de.rwu.easydrop.model.Offer; - -import java.util.ArrayList; -import java.util.List; - public class OfferRetriever { - public List loadOffers() { - return new ArrayList<>(); - } - } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java index 8b814f9..ddbe4b1 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -6,6 +6,7 @@ import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.validation.ProductValidator; @@ -21,9 +22,9 @@ public class ProductRetriever { private DataSourceFactory dataSourceFactory; /** - * @param newDataSourceFactory the dataSourceFactory to set + * @param newDataSourceFactory the WebshopFactory to set */ - public void setDataSourceFactory(final DataSourceFactory newDataSourceFactory) { + public void setWebshopFactory(final DataSourceFactory newDataSourceFactory) { this.dataSourceFactory = newDataSourceFactory; } @@ -31,7 +32,7 @@ public class ProductRetriever { * @param newDataSourceFactory */ public ProductRetriever(final DataSourceFactory newDataSourceFactory) { - this.setDataSourceFactory(newDataSourceFactory); + this.setWebshopFactory(newDataSourceFactory); } /** @@ -83,23 +84,23 @@ public class ProductRetriever { } /** - * Retrieves a product from an API by name of the API so that high-level components do not need to be extended + * Retrieves a product from an API by name of the API so that high-level + * components do not need to be extended * with exact function call names if we extend the list of webshops. * - * @param dataSourceName Data source name, e.g. amazon - * @param productName Product name, translated to the correct product ID for the data source + * @param shop Data source name, e.g. Amazon + * @param productIdentifier Product name, translated to the correct product ID + * for the data source * @return Product from that data source or null if data source not available */ - public Product getProductFromDataSource(Product.webshop dataSourceName, final String productName) { - switch(dataSourceName) { - case AMAZON: - // TODO: Translation from productName to productId (Amazon) needed - return getProductFromAmazon(productName); - case EBAY: - // TODO: Translation from productName to productId (eBay) needed - return getProductFromEbay(productName); - default: - return null; + public Product getProductFromWebshop(final Webshop shop, final String productIdentifier) { + switch (shop) { + case Amazon: + return getProductFromAmazon(productIdentifier); + case eBay: + return getProductFromEbay(productIdentifier); + default: + return null; } } } diff --git a/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java index fc48377..f74f054 100644 --- a/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java +++ b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java @@ -1,8 +1,5 @@ package de.rwu.easydrop.service.validation; -import java.util.HashSet; -import java.util.Set; - import de.rwu.easydrop.exception.InvalidProductException; import de.rwu.easydrop.model.Product; @@ -31,26 +28,8 @@ public final class ProductValidator { if (product.getCurrentPrice() == 0.00) { throw new InvalidProductException("Current price cannot be 0.00"); } - if (!isInValidDataOrigins(product.getDataOrigin())) { - throw new InvalidProductException("Unknown data source"); - } if (product.getProductId().equals("")) { throw new InvalidProductException("Product ID cannot be empty"); } } - - /** - * Checks whether a dataOrigin is within the set of valid ones. - * - * @param dataOrigin like "Amazon" - * @return true if valid - */ - public static boolean isInValidDataOrigins(final String dataOrigin) { - Set validOrigins = new HashSet<>(); - - validOrigins.add("Amazon"); - validOrigins.add("eBay"); - - return validOrigins.contains(dataOrigin); - } } diff --git a/src/main/java/de/rwu/easydrop/util/ProductsConfig.java b/src/main/java/de/rwu/easydrop/util/ProductsConfig.java index b69777f..2295a45 100644 --- a/src/main/java/de/rwu/easydrop/util/ProductsConfig.java +++ b/src/main/java/de/rwu/easydrop/util/ProductsConfig.java @@ -13,6 +13,7 @@ import com.jayway.jsonpath.JsonPathException; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.Webshop; /** * Reads the user-specified catalogue of products. @@ -122,8 +123,8 @@ public final class ProductsConfig { ArrayList> identifiers = JsonPath.read(jsonIdentifiers, "$"); for (HashMap product : identifiers) { - String dataOrigin = product.keySet().iterator().next(); - String identifier = product.get(dataOrigin).toString(); + Webshop dataOrigin = Webshop.fromString(product.keySet().iterator().next()); + String identifier = product.get(dataOrigin.toString()).toString(); Product newProduct = new Product(); newProduct.setDataOrigin(dataOrigin); From 956223bea7a524669ad72f4010e82862794aeea0 Mon Sep 17 00:00:00 2001 From: Leonie Eitze Date: Tue, 27 Jun 2023 02:56:48 +0200 Subject: [PATCH 40/71] Updated tests accordingly --- .../api/client/AbstractDataWriterTest.java | 28 ++++--------- .../api/client/AbstractPurchaserTest.java | 5 ++- .../api/client/AbstractSellerTest.java | 5 ++- .../client/AmazonProductDataSourceTest.java | 29 ++++++------- .../api/client/AmazonPurchaserTest.java | 8 ++-- .../easydrop/api/client/AmazonSellerTest.java | 6 ++- .../api/client/DataSourceFactoryTest.java | 18 ++++---- .../api/client/EbayItemDataSourceTest.java | 17 ++++---- .../api/client/EbayPurchaserTest.java | 6 ++- .../easydrop/api/client/EbaySellerTest.java | 6 ++- .../rwu/easydrop/api/dto/ProductDTOTest.java | 12 +++--- .../java/de/rwu/easydrop/core/CoreTest.java | 27 ------------ .../data/connector/SQLiteConnectorTest.java | 27 ++++-------- .../easydrop/model/ProductCatalogueTest.java | 16 ++++---- .../de/rwu/easydrop/model/ProductTest.java | 12 +++--- .../service/mapping/ProductMapperTest.java | 7 ++-- .../retriever/CatalogueRetrieverTest.java | 41 ++++--------------- .../retriever/ProductRetrieverTest.java | 19 +++++---- .../validation/ProductValidatorTest.java | 41 ++----------------- .../service/writer/CatalogueWriterTest.java | 11 ++--- .../service/writer/ProductWriterTest.java | 5 ++- 21 files changed, 126 insertions(+), 220 deletions(-) diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java index edc8ad8..97b007e 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java @@ -21,6 +21,7 @@ import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.DataWriterException; +import de.rwu.easydrop.model.Webshop; class AbstractDataWriterTest { private static String demoProductId = "whateverId"; @@ -42,8 +43,8 @@ class AbstractDataWriterTest { } @Override - protected String getDataTarget() { - return "Test"; + protected Webshop getDataTarget() { + return Webshop.Amazon; } @Override @@ -53,21 +54,10 @@ class AbstractDataWriterTest { }; } - @Test - void sendPutRequest_wrongDataSource_throwsException() { - ProductDTO dto = new ProductDTO(demoProductId, "Amazon"); - - DataWriterException e = assertThrows(DataWriterException.class, () -> { - writer.sendPutRequest(dto, "testApiType"); - }); - - assertEquals("Product data source and target testApiType API are incompatible.", e.getMessage()); - } - @Test void sendPutRequest_badResponseCode_throwsException() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO(demoProductId, "Test"); + ProductDTO dto = new ProductDTO(demoProductId, Webshop.Amazon); // Set up Mocks AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); @@ -79,13 +69,13 @@ class AbstractDataWriterTest { writer.sendPutRequest(dto, "Sales"); }); - assertEquals("Test Sales API responded with error code 400", e.getMessage()); + assertEquals("Amazon Sales API responded with error code 400", e.getMessage()); } @Test void sendPutRequest_ioException_throwsException() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO(demoProductId, "Test"); + ProductDTO dto = new ProductDTO(demoProductId, Webshop.Amazon); // Set up Mocks AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); @@ -96,13 +86,13 @@ class AbstractDataWriterTest { writer.sendPutRequest(dto, "testApiType"); }); - assertEquals("Couldn't fulfill Test API request", e.getMessage()); + assertEquals("Couldn't fulfill Amazon API request", e.getMessage()); } @Test void sendPutRequest_successfulRequest() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO(demoProductId, "test"); + ProductDTO dto = new ProductDTO(demoProductId, Webshop.Amazon); // Set up Mocks AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); @@ -112,7 +102,7 @@ class AbstractDataWriterTest { HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(mockWriter.getDataTarget()).thenReturn("test"); + when(mockWriter.getDataTarget()).thenReturn(Webshop.Amazon); assertDoesNotThrow(() -> { mockWriter.sendPutRequest(dto, "Purchase"); diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java index f252264..0a62b8e 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java @@ -13,6 +13,7 @@ import java.net.URL; import org.junit.jupiter.api.Test; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Webshop; class AbstractPurchaserTest { private AbstractPurchaser mockPurchaser = mock(AbstractPurchaser.class); @@ -20,7 +21,7 @@ class AbstractPurchaserTest { @Test void purchaseProduct_CorrectApiTypeInException() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO("12345", "test"); + ProductDTO dto = new ProductDTO("12345", Webshop.Amazon); // Set up mocks URL mockURL = mock(URL.class); @@ -29,7 +30,7 @@ class AbstractPurchaserTest { HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(mockPurchaser.getDataTarget()).thenReturn("test"); + when(mockPurchaser.getDataTarget()).thenReturn(Webshop.Amazon); assertDoesNotThrow(() -> { mockPurchaser.purchaseProduct(dto); diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java index eb7e595..e907a37 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java @@ -13,6 +13,7 @@ import java.net.URL; import org.junit.jupiter.api.Test; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Webshop; class AbstractSellerTest { private AbstractSeller mockSeller = mock(AbstractSeller.class); @@ -20,7 +21,7 @@ class AbstractSellerTest { @Test void purchaseProduct_CorrectApiTypeInException() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO("12345", "test"); + ProductDTO dto = new ProductDTO("12345", Webshop.Amazon); // Set up mocks URL mockURL = mock(URL.class); @@ -29,7 +30,7 @@ class AbstractSellerTest { HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(mockSeller.getDataTarget()).thenReturn("test"); + when(mockSeller.getDataTarget()).thenReturn(Webshop.Amazon); assertDoesNotThrow(() -> { mockSeller.sellProduct(dto); diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java index 938413d..7735bd9 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java @@ -21,6 +21,7 @@ import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.DataSourceException; +import de.rwu.easydrop.model.Webshop; class AmazonProductDataSourceTest { @@ -28,7 +29,7 @@ class AmazonProductDataSourceTest { private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static String demoDataOrigin = "Amazon"; + private static Webshop demoDataOrigin = Webshop.Amazon; private static String demoProductId = "whateverId"; @BeforeEach @@ -138,7 +139,7 @@ class AmazonProductDataSourceTest { // Verify the product DTO properties assertEquals(demoProductId, result.getProductId()); - assertEquals("Amazon", result.getDataOrigin()); + assertEquals(Webshop.Amazon, result.getDataOrigin()); assertEquals(true, result.isAvailable()); assertEquals(10.0, result.getCurrentPrice()); assertEquals(2.5, result.getDeliveryPrice()); @@ -149,18 +150,18 @@ class AmazonProductDataSourceTest { void testGetProductDTOById_failedRequest() throws IOException { // Set up the test environment - AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + AmazonProductDataSource DataSource = mock(AmazonProductDataSource.class); URL mockURL = mock(URL.class); - when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin); - when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); - when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); + when(DataSource.getDataOrigin()).thenReturn(demoDataOrigin); + 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 DataSourceException exception = assertThrows(DataSourceException.class, () -> { - dataSource.getProductDTOById(demoProductId); + DataSource.getProductDTOById(demoProductId); }); // Verify the exception message @@ -170,12 +171,12 @@ class AmazonProductDataSourceTest { @Test void testGetProductDTOById_ioException() throws IOException { // Set up the test environment - AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + AmazonProductDataSource DataSource = mock(AmazonProductDataSource.class); URL mockURL = mock(URL.class); - when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin); - when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); - when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); - when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod(); + when(DataSource.getDataOrigin()).thenReturn(demoDataOrigin); + 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); @@ -183,7 +184,7 @@ class AmazonProductDataSourceTest { // Invoke the method and verify the exception DataSourceException exception = assertThrows(DataSourceException.class, () -> { - dataSource.getProductDTOById(demoProductId); + DataSource.getProductDTOById(demoProductId); }); // Verify the exception message @@ -192,7 +193,7 @@ class AmazonProductDataSourceTest { @Test void getDataOrigin_ReturnsExpectedDataOrigin() { - String dataOrigin = demoDataSource.getDataOrigin(); + Webshop dataOrigin = demoDataSource.getDataOrigin(); assertEquals(demoDataOrigin, dataOrigin); } diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java index ec1e1d6..1386e97 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java @@ -1,7 +1,6 @@ package de.rwu.easydrop.api.client; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import java.lang.reflect.Field; import java.net.MalformedURLException; @@ -12,15 +11,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; -import de.rwu.easydrop.api.dto.ProductDTO; -import de.rwu.easydrop.exception.DataWriterException; +import de.rwu.easydrop.model.Webshop; class AmazonPurchaserTest { private AmazonPurchaser demoPurchaser; private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static String demoDataTarget = "Amazon"; + private static Webshop demoDataTarget = Webshop.Amazon; @BeforeEach void setup() { @@ -72,7 +70,7 @@ class AmazonPurchaserTest { @Test void getDataTarget_ReturnsExpectedDataOrigin() { - String dataTarget = demoPurchaser.getDataTarget(); + Webshop dataTarget = demoPurchaser.getDataTarget(); assertEquals(demoDataTarget, dataTarget); } diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java index 06176cc..4ff6d55 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java @@ -11,12 +11,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; +import de.rwu.easydrop.model.Webshop; + class AmazonSellerTest { private AmazonSeller demoSeller; private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static String demoDataTarget = "Amazon"; + private static Webshop demoDataTarget = Webshop.Amazon; @BeforeEach void setup() { @@ -68,7 +70,7 @@ class AmazonSellerTest { @Test void getDataTarget_ReturnsExpectedDataOrigin() { - String dataTarget = demoSeller.getDataTarget(); + Webshop dataTarget = demoSeller.getDataTarget(); assertEquals(demoDataTarget, dataTarget); } diff --git a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java index f1c85b0..5196c8b 100644 --- a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java @@ -21,7 +21,7 @@ class DataSourceFactoryTest { @Mock private Config config; - private DataSourceFactory dataSourceFactory; + private DataSourceFactory DataSourceFactory; @BeforeEach void setUp() throws ConfigurationException { @@ -30,31 +30,31 @@ class DataSourceFactoryTest { when(config.getProperty("AMAZON_API_KEY")).thenReturn("amazon-api-key"); when(config.getProperty("EBAY_API_URL")).thenReturn("https://api.ebay.com"); when(config.getProperty("EBAY_API_KEY")).thenReturn("ebay-api-key"); - dataSourceFactory = new DataSourceFactory(config); + DataSourceFactory = new DataSourceFactory(config); } @Test void createAmazonProductDataSource_ReturnsAmazonProductDataSource() { // Act - AmazonProductDataSource dataSource = dataSourceFactory.createAmazonProductDataSource(); + AmazonProductDataSource DataSource = DataSourceFactory.createAmazonProductDataSource(); // Assert - assertEquals("amazon-api-key", dataSource.getApiKey()); + assertEquals("amazon-api-key", DataSource.getApiKey()); } @Test void createEbayItemDataSource_ReturnsEbayItemDataSource() { // Act - EbayItemDataSource dataSource = dataSourceFactory.createEbayItemDataSource(); + EbayItemDataSource DataSource = DataSourceFactory.createEbayItemDataSource(); // Assert - assertEquals("ebay-api-key", dataSource.getApiKey()); + assertEquals("ebay-api-key", DataSource.getApiKey()); } @Test void createProductPersistenceDataSource_NullPersistence() { PersistenceException exception = assertThrows(PersistenceException.class, () -> { - dataSourceFactory.createProductPersistenceDataSource(); + DataSourceFactory.createProductPersistenceDataSource(); }); assertEquals("Persistence is not set", exception.getMessage()); @@ -62,8 +62,8 @@ class DataSourceFactoryTest { @Test void createProductPersistenceDataSource_WorkingPersistence() { - dataSourceFactory.setPersistence(new SQLiteConnector(new SQLiteDataSource())); + DataSourceFactory.setPersistence(new SQLiteConnector(new SQLiteDataSource())); - assertDoesNotThrow(() -> dataSourceFactory.createProductPersistenceDataSource()); + assertDoesNotThrow(() -> DataSourceFactory.createProductPersistenceDataSource()); } } diff --git a/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java index 9a6ed45..a311c73 100644 --- a/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java @@ -19,13 +19,14 @@ import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.DataSourceException; +import de.rwu.easydrop.model.Webshop; class EbayItemDataSourceTest { private EbayItemDataSource demoDataSource; private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static String demoDataOrigin = "eBay"; + private static Webshop demoDataOrigin = Webshop.eBay; private static String demoQuery = "iPhone"; @BeforeEach @@ -82,7 +83,7 @@ class EbayItemDataSourceTest { ProductDTO result = demoDataSource.buildProductDTO(product, json); - assertEquals("eBay", result.getDataOrigin()); + assertEquals(Webshop.eBay, result.getDataOrigin()); assertEquals(false, result.isAvailable()); // Default value for boolean assertEquals(0.0, result.getCurrentPrice()); // Default value for double assertEquals(0.0, result.getDeliveryPrice()); // Default value for double @@ -91,7 +92,7 @@ class EbayItemDataSourceTest { @Test void getDataOrigin_ReturnsExpectedDataOrigin() { - String dataOrigin = demoDataSource.getDataOrigin(); + Webshop dataOrigin = demoDataSource.getDataOrigin(); assertEquals(demoDataOrigin, dataOrigin); } @@ -105,18 +106,18 @@ class EbayItemDataSourceTest { void testGetProductDTOById_failedRequest() throws IOException { // Set up the test environment - EbayItemDataSource dataSource = mock(EbayItemDataSource.class); + EbayItemDataSource DataSource = mock(EbayItemDataSource.class); URL mockURL = mock(URL.class); - when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin); - when(dataSource.createApiUrl(demoQuery)).thenReturn(mockURL); - when(dataSource.getProductDTOById(demoQuery)).thenCallRealMethod(); + when(DataSource.getDataOrigin()).thenReturn(demoDataOrigin); + when(DataSource.createApiUrl(demoQuery)).thenReturn(mockURL); + when(DataSource.getProductDTOById(demoQuery)).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 DataSourceException exception = assertThrows(DataSourceException.class, () -> { - dataSource.getProductDTOById(demoQuery); + DataSource.getProductDTOById(demoQuery); }); // Verify the exception message diff --git a/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java index 238e2c5..2b2104c 100644 --- a/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java @@ -11,12 +11,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; +import de.rwu.easydrop.model.Webshop; + class EbayPurchaserTest { private EbayPurchaser demoPurchaser; private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static String demoDataTarget = "eBay"; + private static Webshop demoDataTarget = Webshop.eBay; @BeforeEach void setup() { @@ -65,7 +67,7 @@ class EbayPurchaserTest { @Test void getDataTarget_ReturnsExpectedDataOrigin() { - String dataTarget = demoPurchaser.getDataTarget(); + Webshop dataTarget = demoPurchaser.getDataTarget(); assertEquals(demoDataTarget, dataTarget); } diff --git a/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java b/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java index 978aa63..0fe0df0 100644 --- a/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java @@ -11,12 +11,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; +import de.rwu.easydrop.model.Webshop; + class EbaySellerTest { private EbaySeller demoSeller; private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static String demoDataTarget = "eBay"; + private static Webshop demoDataTarget = Webshop.eBay; @BeforeEach void setup() { @@ -65,7 +67,7 @@ class EbaySellerTest { @Test void getDataTarget_ReturnsExpectedDataOrigin() { - String dataTarget = demoSeller.getDataTarget(); + Webshop dataTarget = demoSeller.getDataTarget(); assertEquals(demoDataTarget, dataTarget); } diff --git a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java index 1e16d7c..647babb 100644 --- a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java +++ b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java @@ -6,13 +6,15 @@ import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; +import de.rwu.easydrop.model.Webshop; + class ProductDTOTest { @Test void constructor_SetsProductIdAndDataOrigin() { // Arrange String productId = "12345"; - String dataOrigin = "Amazon"; + Webshop dataOrigin = Webshop.Amazon; // Act ProductDTO productDTO = new ProductDTO(productId, dataOrigin); @@ -25,19 +27,19 @@ class ProductDTOTest { @Test void gettersAndSetters_WorkAsExpected() { // Arrange - ProductDTO productDTO = new ProductDTO("12345", "Amazon"); + ProductDTO productDTO = new ProductDTO("12345", Webshop.Amazon); // Act and Assert assertEquals("12345", productDTO.getProductId()); - assertEquals("Amazon", productDTO.getDataOrigin()); + assertEquals(Webshop.Amazon, productDTO.getDataOrigin()); // Modify fields productDTO.setProductId("54321"); - productDTO.setDataOrigin("eBay"); + productDTO.setDataOrigin(Webshop.eBay); // Assert assertEquals("54321", productDTO.getProductId()); - assertEquals("eBay", productDTO.getDataOrigin()); + assertEquals(Webshop.eBay, productDTO.getDataOrigin()); } @Test diff --git a/src/test/java/de/rwu/easydrop/core/CoreTest.java b/src/test/java/de/rwu/easydrop/core/CoreTest.java index e37170f..bf62c6d 100644 --- a/src/test/java/de/rwu/easydrop/core/CoreTest.java +++ b/src/test/java/de/rwu/easydrop/core/CoreTest.java @@ -1,31 +1,4 @@ package de.rwu.easydrop.core; -import java.util.List; -import javax.naming.ConfigurationException; - -import org.junit.jupiter.api.Test; - -import de.rwu.easydrop.api.client.DataSourceFactory; - -import de.rwu.easydrop.model.ProductCatalogue; -import de.rwu.easydrop.service.retriever.CatalogueRetriever; -import de.rwu.easydrop.service.retriever.ProductRetriever; -import de.rwu.easydrop.util.Config; -import de.rwu.easydrop.util.ProductsConfig; - public class CoreTest { - @Test - void testRunCore() throws ConfigurationException{ - Config config = Config.getInstance(); - ProductsConfig pConfig = ProductsConfig.getInstance(); - DataSourceFactory dataSourceFactory = new DataSourceFactory(config); - ProductRetriever retriever = new ProductRetriever(dataSourceFactory); - CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); - - catRetriever.loadCatalogues(); - List pCats = catRetriever.getProductCatalogues(); - Core core1=new Core(); - core1.runCore(pCats); - - } } diff --git a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java index 2e54299..9c513db 100644 --- a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java +++ b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java @@ -22,6 +22,7 @@ import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.PersistenceException; +import de.rwu.easydrop.model.Webshop; @TestInstance(Lifecycle.PER_CLASS) class SQLiteConnectorTest { @@ -45,8 +46,8 @@ class SQLiteConnectorTest { void saveProduct_ValidProduct_SuccessfullySaved() { // Arrange sqliteConnector.clearData(); - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); - ProductDTO.setDataOrigin("Amazon"); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.Amazon); + ProductDTO.setDataOrigin(Webshop.Amazon); ProductDTO.setProductId(TEST_PRODUCT_ID); ProductDTO.setCurrentPrice(9.99); ProductDTO.setMerchant("Sample Merchant"); @@ -59,7 +60,7 @@ class SQLiteConnectorTest { // Assert ProductDTO savedProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID); assertNotNull(savedProductDTO); - assertEquals("Amazon", savedProductDTO.getDataOrigin()); + assertEquals(Webshop.Amazon, savedProductDTO.getDataOrigin()); assertEquals(TEST_PRODUCT_ID, savedProductDTO.getProductId()); assertEquals(9.99, savedProductDTO.getCurrentPrice()); assertEquals("Sample Merchant", savedProductDTO.getMerchant()); @@ -78,7 +79,7 @@ class SQLiteConnectorTest { // Assert assertNotNull(ProductDTO); - assertEquals("Amazon", ProductDTO.getDataOrigin()); + assertEquals(Webshop.Amazon, ProductDTO.getDataOrigin()); assertEquals(TEST_PRODUCT_ID, ProductDTO.getProductId()); assertEquals(9.99, ProductDTO.getCurrentPrice()); assertEquals("Sample Merchant", ProductDTO.getMerchant()); @@ -113,7 +114,7 @@ class SQLiteConnectorTest { @Test void saveProduct_ThrowsPersistenceException_OnSQLException() throws SQLException { // Arrange - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.Amazon); sqliteConnector.setDb(mockDataSource); doThrow(SQLException.class).when(mockDataSource).getConnection(); @@ -143,7 +144,7 @@ class SQLiteConnectorTest { } private void insertSampleProduct() { - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.Amazon); ProductDTO.setCurrentPrice(9.99); ProductDTO.setMerchant("Sample Merchant"); ProductDTO.setDeliveryPrice(2.50); @@ -151,18 +152,6 @@ class SQLiteConnectorTest { sqliteConnector.saveProduct(ProductDTO); } - @Test - void getDataOrigin_ReturnsCorrectDataOrigin() { - // Arrange - SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); - - // Act - String dataOrigin = connector.getDataOrigin(); - - // Assert - assertEquals("SQLite", dataOrigin); - } - @Test void getApiKey_UnsupportedOperationExceptionThrown() { // Arrange @@ -176,7 +165,7 @@ class SQLiteConnectorTest { void buildProductDTO_UnsupportedOperationExceptionThrown() { // Arrange SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); - ProductDTO product = new ProductDTO("ASIN123", "Amazon"); + ProductDTO product = new ProductDTO("ASIN123", Webshop.Amazon); String json = "{\"productId\":\"ASIN123\",\"dataOrigin\":\"Amazon\"}"; // Act and Assert diff --git a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java index c13dcfe..c83f84f 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java @@ -19,7 +19,7 @@ class ProductCatalogueTest { Product product = new Product(); product.setProductId("12345"); product.setMerchant("AmazonSeller"); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.Amazon); productCatalogue.addProduct(product); List products = productCatalogue.getProducts(); @@ -32,13 +32,13 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.Amazon); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); product2.setMerchant("eBaySeller"); - product2.setDataOrigin("eBay"); + product2.setDataOrigin(Webshop.eBay); productCatalogue.addProduct(product2); productCatalogue.removeProduct(product1); @@ -53,13 +53,13 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.Amazon); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); - product2.setMerchant("eBay"); - product2.setDataOrigin("eBay"); + product2.setMerchant("ebayMerchant"); + product2.setDataOrigin(Webshop.eBay); productCatalogue.addProduct(product2); productCatalogue.clearProducts(); @@ -73,13 +73,13 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.Amazon); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); product2.setMerchant("eBaySeller"); - product2.setDataOrigin("eBay"); + product2.setDataOrigin(Webshop.eBay); productCatalogue.addProduct(product2); String expectedString = "Product Catalogue: GPU\n" + diff --git a/src/test/java/de/rwu/easydrop/model/ProductTest.java b/src/test/java/de/rwu/easydrop/model/ProductTest.java index cf79119..2c5c1c2 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductTest.java @@ -10,7 +10,7 @@ class ProductTest { @Test void testToString1() { Product product1 = new Product(); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.Amazon); product1.setProductId("12345"); product1.setMerchant("Merchant A"); product1.setCurrentPrice(19.99); @@ -25,7 +25,7 @@ class ProductTest { @Test void testToString2() { Product product2 = new Product(); - product2.setDataOrigin("eBay"); + product2.setDataOrigin(Webshop.eBay); product2.setProductId("67890"); product2.setMerchant("Merchant B"); product2.setCurrentPrice(9.99); @@ -41,7 +41,7 @@ class ProductTest { void gettersAndSetters_WorkAsExpected() { // Arrange Product product = new Product(); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.Amazon); product.setProductId("12345"); product.setCurrentPrice(9.99); product.setMerchant("Example Merchant"); @@ -49,7 +49,7 @@ class ProductTest { product.setAvailable(true); // Act and Assert - assertEquals("Amazon", product.getDataOrigin()); + assertEquals(Webshop.Amazon, product.getDataOrigin()); assertEquals("12345", product.getProductId()); assertEquals(9.99, product.getCurrentPrice()); assertEquals("Example Merchant", product.getMerchant()); @@ -57,7 +57,7 @@ class ProductTest { assertTrue(product.isAvailable()); // Modify fields - product.setDataOrigin("eBay"); + product.setDataOrigin(Webshop.eBay); product.setProductId("54321"); product.setCurrentPrice(19.99); product.setMerchant("New Merchant"); @@ -65,7 +65,7 @@ class ProductTest { product.setAvailable(false); // Assert - assertEquals("eBay", product.getDataOrigin()); + assertEquals(Webshop.eBay, product.getDataOrigin()); assertEquals("54321", product.getProductId()); assertEquals(19.99, product.getCurrentPrice()); assertEquals("New Merchant", product.getMerchant()); diff --git a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java index 6239dbf..9212a24 100644 --- a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java +++ b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; class ProductMapperTest { @Test @@ -47,7 +48,7 @@ class ProductMapperTest { } private ProductDTO createProductDTO() { - ProductDTO dto = new ProductDTO("12345", "Amazon"); + ProductDTO dto = new ProductDTO("12345", Webshop.Amazon); dto.setAvailable(true); dto.setCurrentPrice(9.99); dto.setDeliveryPrice(2.50); @@ -60,7 +61,7 @@ class ProductMapperTest { // Arrange Product product = new Product(); product.setProductId("12345"); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.Amazon); product.setAvailable(true); product.setCurrentPrice(9.99); product.setDeliveryPrice(2.50); @@ -71,7 +72,7 @@ class ProductMapperTest { // Assert assertEquals("12345", dto.getProductId()); - assertEquals("Amazon", dto.getDataOrigin()); + assertEquals(Webshop.Amazon, dto.getDataOrigin()); assertTrue(dto.isAvailable()); assertEquals(9.99, dto.getCurrentPrice()); assertEquals(2.50, dto.getDeliveryPrice()); diff --git a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java index acaca80..edd772a 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java @@ -1,7 +1,6 @@ package de.rwu.easydrop.service.retriever; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -14,9 +13,9 @@ import javax.naming.ConfigurationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import de.rwu.easydrop.exception.InvalidProductException; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.util.ProductsConfig; class CatalogueRetrieverTest { @@ -39,20 +38,20 @@ class CatalogueRetrieverTest { // Create a sample product catalogue with two products ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue"); Product product1 = new Product(); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.Amazon); product1.setProductId("ASIN1"); productCatalogue.addProduct(product1); Product product2 = new Product(); - product2.setDataOrigin("eBay"); + product2.setDataOrigin(Webshop.eBay); product2.setProductId("ProductID2"); productCatalogue.addProduct(product2); productCatalogues.add(productCatalogue); // Mock the methods when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues); - when(productRetriever.getProductFromAmazon("ASIN1")).thenReturn(product1); - when(productRetriever.getProductFromEbay("ProductID2")).thenReturn(product2); + when(productRetriever.getProductFromWebshop(Webshop.Amazon, "ASIN1")).thenReturn(product1); + when(productRetriever.getProductFromWebshop(Webshop.eBay, "ProductID2")).thenReturn(product2); // Act catalogueRetriever.loadCatalogues(); @@ -71,33 +70,7 @@ class CatalogueRetrieverTest { // Verify the method invocations verify(productsConfig).loadConfig(); - verify(productRetriever).getProductFromAmazon("ASIN1"); - verify(productRetriever).getProductFromEbay("ProductID2"); - } - - @Test - void loadCatalogues_ValidConfig_CataloguesLoaded_InvalidProduct() throws ConfigurationException { - // Arrange - List productCatalogues = new ArrayList<>(); - - // Create a sample product catalogue - ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue"); - - Product product = new Product(); - product.setDataOrigin(""); - product.setProductId("ProductID1"); - productCatalogue.addProduct(product); - productCatalogues.add(productCatalogue); - - // Mock the methods - when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues); - when(productRetriever.getProductFromAmazon("ProductID1")).thenReturn(product); - - // Act and Assert - InvalidProductException exception = assertThrows(InvalidProductException.class, () -> { - catalogueRetriever.loadCatalogues(); - }); - - assertEquals("Product data origin is invalid", exception.getMessage()); + verify(productRetriever).getProductFromWebshop(Webshop.Amazon, "ASIN1"); + verify(productRetriever).getProductFromWebshop(Webshop.eBay, "ProductID2"); } } diff --git a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java index 32afecc..5fee30f 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java @@ -19,13 +19,14 @@ import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.util.Config; class ProductRetrieverTest { @Mock private Config config; @Mock - private DataSourceFactory dataSourceFactory; + private DataSourceFactory DataSourceFactory; @Mock private AmazonProductDataSource amazonDataSource; @Mock @@ -44,19 +45,19 @@ class ProductRetrieverTest { MockitoAnnotations.openMocks(this); when(config.getProperty("AMAZON_API_URL")).thenReturn("https://api.amazon.com"); when(config.getProperty("AMAZON_API_KEY")).thenReturn("amazon-api-key"); - dataSourceFactory.setConfig(config); - productRetriever = new ProductRetriever(dataSourceFactory); + DataSourceFactory.setConfig(config); + productRetriever = new ProductRetriever(DataSourceFactory); } @Test void getProductFromAmazon_ReturnsProduct() { // Arrange String asin = "B01234ABC"; - when(dataSourceFactory.createAmazonProductDataSource()).thenReturn(amazonDataSource); + when(DataSourceFactory.createAmazonProductDataSource()).thenReturn(amazonDataSource); when(amazonDataSource.getProductDTOById(asin)).thenReturn(productDTO); when(productDTO.getProductId()).thenReturn(asin); when(productDTO.getCurrentPrice()).thenReturn(9.99); - when(productDTO.getDataOrigin()).thenReturn("Amazon"); + when(productDTO.getDataOrigin()).thenReturn(Webshop.Amazon); // Act Product result = productRetriever.getProductFromAmazon(asin); @@ -72,11 +73,11 @@ class ProductRetrieverTest { void getProductFromEbay_ReturnsProduct() { // Arrange String productQuery = "MySearchQuery"; - when(dataSourceFactory.createEbayItemDataSource()).thenReturn(ebayDataSource); + when(DataSourceFactory.createEbayItemDataSource()).thenReturn(ebayDataSource); when(ebayDataSource.getProductDTOById(productQuery)).thenReturn(productDTO); when(productDTO.getProductId()).thenReturn(productQuery); when(productDTO.getCurrentPrice()).thenReturn(9.99); - when(productDTO.getDataOrigin()).thenReturn("eBay"); + when(productDTO.getDataOrigin()).thenReturn(Webshop.eBay); // Act Product result = productRetriever.getProductFromEbay(productQuery); @@ -92,11 +93,11 @@ class ProductRetrieverTest { void getProductFromPersistence_ValidProductId_ReturnsProduct() { // Arrange String productId = "123"; - when(dataSourceFactory.createProductPersistenceDataSource()).thenReturn(persistence); + when(DataSourceFactory.createProductPersistenceDataSource()).thenReturn(persistence); when(persistence.getProductDTOById(productId)).thenReturn(productDTO); when(productDTO.getProductId()).thenReturn(productId); when(productDTO.getCurrentPrice()).thenReturn(9.99); - when(productDTO.getDataOrigin()).thenReturn("Amazon"); + when(productDTO.getDataOrigin()).thenReturn(Webshop.Amazon); // Act Product result = productRetriever.getProductFromPersistence(productId); diff --git a/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java index 7827bbb..1ad2f6b 100644 --- a/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java +++ b/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java @@ -1,7 +1,6 @@ package de.rwu.easydrop.service.validation; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -16,6 +15,7 @@ import org.junit.jupiter.params.provider.MethodSource; import de.rwu.easydrop.exception.InvalidProductException; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; class ProductValidatorTest { @@ -38,37 +38,13 @@ class ProductValidatorTest { // Arrange Product product = new Product(); product.setCurrentPrice(9.99); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.Amazon); product.setProductId("12345"); // Act and Assert assertDoesNotThrow(() -> ProductValidator.validate(product)); } - @Test - void isInValidDataOrigins_ValidDataOrigin_ReturnsTrue() { - // Arrange - String dataOrigin = "Amazon"; - - // Act - boolean result = ProductValidator.isInValidDataOrigins(dataOrigin); - - // Assert - assertTrue(result); - } - - @Test - void isInValidDataOrigins_InvalidDataOrigin_ReturnsFalse() { - // Arrange - String dataOrigin = "UnknownOrigin"; - - // Act - boolean result = ProductValidator.isInValidDataOrigins(dataOrigin); - - // Assert - assertFalse(result); - } - @ParameterizedTest @MethodSource("invalidProductProvider") void validate_InvalidProduct_ThrowsInvalidProductException(Product product) { @@ -79,22 +55,13 @@ class ProductValidatorTest { static Stream invalidProductProvider() { return Stream.of( createProductWithZeroPrice(), - createProductWithUnknownDataOrigin(), createProductWithEmptyProductId()); } private static Product createProductWithZeroPrice() { Product product = new Product(); product.setCurrentPrice(0.00); - product.setDataOrigin("Amazon"); - product.setProductId("12345"); - return product; - } - - private static Product createProductWithUnknownDataOrigin() { - Product product = new Product(); - product.setCurrentPrice(9.99); - product.setDataOrigin("UnknownOrigin"); + product.setDataOrigin(Webshop.Amazon); product.setProductId("12345"); return product; } @@ -102,7 +69,7 @@ class ProductValidatorTest { private static Product createProductWithEmptyProductId() { Product product = new Product(); product.setCurrentPrice(9.99); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.Amazon); product.setProductId(""); return product; } diff --git a/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java index 71fdff5..c590733 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java @@ -16,6 +16,7 @@ import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.Webshop; class CatalogueWriterTest { @@ -46,12 +47,12 @@ class CatalogueWriterTest { List catalogues = new ArrayList<>(); ProductCatalogue catalogue1 = new ProductCatalogue("Catalogue 1", "Sample catalogue 1"); - catalogue1.addProduct(createSampleProduct("Amazon", "ID 1")); - catalogue1.addProduct(createSampleProduct("eBay", "ID 2")); + catalogue1.addProduct(createSampleProduct(Webshop.Amazon, "ID 1")); + catalogue1.addProduct(createSampleProduct(Webshop.eBay, "ID 2")); ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue 2", "Sample catalogue 2"); - catalogue2.addProduct(createSampleProduct("Amazon", "ID 3")); - catalogue2.addProduct(createSampleProduct("eBay", "ID 4")); + catalogue2.addProduct(createSampleProduct(Webshop.Amazon, "ID 3")); + catalogue2.addProduct(createSampleProduct(Webshop.eBay, "ID 4")); catalogues.add(catalogue1); catalogues.add(catalogue2); @@ -59,7 +60,7 @@ class CatalogueWriterTest { return catalogues; } - private Product createSampleProduct(String dataOrigin, String productId) { + private Product createSampleProduct(Webshop dataOrigin, String productId) { Product product = new Product(); product.setDataOrigin(dataOrigin); product.setProductId(productId); diff --git a/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java index aab0cdd..e2bd870 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java @@ -12,6 +12,7 @@ import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; class ProductWriterTest { @@ -32,7 +33,7 @@ class ProductWriterTest { // Arrange Product product = new Product(); product.setProductId("12345"); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.Amazon); product.setCurrentPrice(9.99); // Act @@ -47,7 +48,7 @@ class ProductWriterTest { // Arrange Product product = new Product(); product.setProductId(""); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.Amazon); // Act and Assert assertThrows(Exception.class, () -> productWriter.writeProductToPersistence(product)); From d01c4d0b1d520c6b8bddbe49829fffe474a59543 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 27 Jun 2023 05:23:43 +0200 Subject: [PATCH 41/71] Connected application components --- src/main/java/de/rwu/easydrop/Main.java | 35 +----- .../api/client/AmazonProductDataSource.java | 2 + .../api/client/DataSourceFactory.java | 26 ---- .../api/client/EbayItemDataSource.java | 2 + .../de/rwu/easydrop/api/dto/OfferDTO.java | 37 ++---- .../de/rwu/easydrop/api/dto/ProductDTO.java | 5 + src/main/java/de/rwu/easydrop/core/Core.java | 64 +++++++--- .../de/rwu/easydrop/core/OfferIdentifier.java | 83 ------------- .../de/rwu/easydrop/core/OfferReviewer.java | 81 ------------ .../de/rwu/easydrop/core/OfferUpdater.java | 110 ----------------- .../connector/AbstractProductPersistence.java | 34 ------ ...ce.java => OfferPersistenceInterface.java} | 8 +- .../ProductPersistenceInterface.java | 30 +++++ .../data/connector/SQLiteConnector.java | 115 ++++++++++++++---- .../exception/InvalidOfferException.java | 22 ++++ .../java/de/rwu/easydrop/model/Offer.java | 32 ++--- .../java/de/rwu/easydrop/model/Product.java | 5 + .../rwu/easydrop/model/ProductCatalogue.java | 2 +- .../easydrop/service/mapping/OfferMapper.java | 11 +- .../service/mapping/ProductMapper.java | 2 + ...OrderManager.java => OfferIdentifier.java} | 54 ++++---- .../processing}/OfferProvisioner.java | 48 +++----- .../service/retriever/CatalogueRetriever.java | 13 +- .../service/retriever/OfferRetriever.java | 37 +++++- .../service/retriever/ProductRetriever.java | 21 ++-- .../service/validation/OfferValidator.java | 19 ++- .../service/writer/CatalogueWriter.java | 8 +- .../easydrop/service/writer/OfferWriter.java | 10 +- .../service/writer/ProductWriter.java | 8 +- .../de/rwu/easydrop/util/FormattingUtil.java | 10 ++ .../java/de/rwu/easydrop/util/Timestamp.java | 29 +++++ 31 files changed, 402 insertions(+), 561 deletions(-) delete mode 100644 src/main/java/de/rwu/easydrop/core/OfferIdentifier.java delete mode 100644 src/main/java/de/rwu/easydrop/core/OfferReviewer.java delete mode 100644 src/main/java/de/rwu/easydrop/core/OfferUpdater.java delete mode 100644 src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java rename src/main/java/de/rwu/easydrop/data/connector/{AbstractOfferPersistence.java => OfferPersistenceInterface.java} (64%) create mode 100644 src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java create mode 100644 src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java rename src/main/java/de/rwu/easydrop/service/processing/{OrderManager.java => OfferIdentifier.java} (64%) rename src/main/java/de/rwu/easydrop/{core => service/processing}/OfferProvisioner.java (57%) create mode 100644 src/main/java/de/rwu/easydrop/util/Timestamp.java diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index c8f4f1e..db1fedb 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -1,23 +1,8 @@ package de.rwu.easydrop; -import java.util.List; - import javax.naming.ConfigurationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sqlite.SQLiteDataSource; - -import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.core.Core; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; -import de.rwu.easydrop.data.connector.SQLiteConnector; -import de.rwu.easydrop.model.ProductCatalogue; -import de.rwu.easydrop.service.retriever.CatalogueRetriever; -import de.rwu.easydrop.service.retriever.ProductRetriever; -import de.rwu.easydrop.service.writer.CatalogueWriter; -import de.rwu.easydrop.util.Config; -import de.rwu.easydrop.util.ProductsConfig; /** * Kickoff point for the service. @@ -25,11 +10,6 @@ import de.rwu.easydrop.util.ProductsConfig; * @since 0.1.0 */ public final class Main { - /** - * Logger for main process. - */ - private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); - /** * Prevents unwanted instantiation. */ @@ -43,19 +23,6 @@ public final class Main { * @param args */ public static void main(final String[] args) throws ConfigurationException { - Config config = Config.getInstance(); - ProductsConfig pConfig = ProductsConfig.getInstance(); - DataSourceFactory dataSourceFactory = new DataSourceFactory(config); - ProductRetriever retriever = new ProductRetriever(dataSourceFactory); - CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); - AbstractProductPersistence db = new SQLiteConnector(new SQLiteDataSource()); - CatalogueWriter catWriter = new CatalogueWriter(db); - - catRetriever.loadCatalogues(); - List pCats = catRetriever.getProductCatalogues(); - catWriter.writeCatalogues(pCats); - - Core core = new Core(); - core.runCore(pCats); + Core.run(); } } diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java index 404398c..06c806e 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java @@ -9,6 +9,7 @@ import com.jayway.jsonpath.ReadContext; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.util.Timestamp; /** * Interface to an Amazon data source. @@ -61,6 +62,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { product.setDeliveryPrice( ctx.read(root + "shippingOptions[0].shippingCost.value.amount", double.class)); product.setMerchant(ctx.read(root + "merchant.name", String.class)); + product.setLastUpdate(Timestamp.now()); } catch (PathNotFoundException e) { // Pass, allow incomplete ProductDTO to pass for later validation } diff --git a/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java index 490466c..ba79ab5 100644 --- a/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java +++ b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java @@ -2,8 +2,6 @@ package de.rwu.easydrop.api.client; import javax.naming.ConfigurationException; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; -import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.util.Config; /** @@ -17,17 +15,6 @@ public class DataSourceFactory { * The data source config. */ private Config config; - /** - * Persistence interface. - */ - private AbstractProductPersistence persistence = null; - - /** - * @param newPersistence the persistence to set - */ - public void setPersistence(final AbstractProductPersistence newPersistence) { - this.persistence = newPersistence; - } /** * @param newConfig the config to set @@ -65,17 +52,4 @@ public class DataSourceFactory { String apiKey = config.getProperty("EBAY_API_KEY"); return new EbayItemDataSource(apiUrl, apiKey); } - - /** - * Creates a persistence data source. - * - * @return ProductPersistenceInterface - */ - public AbstractProductPersistence createProductPersistenceDataSource() { - if (persistence == null) { - throw new PersistenceException("Persistence is not set"); - } - - return persistence; - } } diff --git a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java index 5e62394..0b1ed40 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java @@ -9,6 +9,7 @@ import com.jayway.jsonpath.ReadContext; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.util.Timestamp; /** * Interface to an eBay data source. @@ -70,6 +71,7 @@ public final class EbayItemDataSource extends AbstractDataSource { product.setDeliveryPrice( ctx.read(root + "shippingOptions[0].shippingCost.value", double.class)); product.setMerchant(ctx.read(root + "seller.username", String.class)); + product.setLastUpdate(Timestamp.now()); } catch (PathNotFoundException e) { // Pass, allow incomplete ProductDTO to pass for later validation } diff --git a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java index c2d680f..8ffd6a8 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java @@ -1,52 +1,31 @@ package de.rwu.easydrop.api.dto; import lombok.Data; -import de.rwu.easydrop.model.Product; -import java.util.Date; /* - * Offer data transfer object + * Offer data transfer object. + * + * @since 0.3.0 */ @Data public class OfferDTO { /** - * Platform internal offer identifier. + * ID of the offer, built from identifiers of the source platforms. */ private String offerId; /** * The product that our software buys. */ - private Product sourceProduct; + private ProductDTO sourceProduct; /** * The product that our software sells. */ - private Product saleProduct; + private ProductDTO targetProduct; /** - * Date of the creation of the offer - * NOTE: Use Timestamp? - * https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html + * Date of last update of the offer. */ - private Date creationDate; - - /** - * Date of last update of the offer on the destination website (API). - */ - private Date upDate; - - /** - * Date of last check if offer is still valid. - */ - private Date checkDate; - - /** - * Creates OfferDTO instance. - * - * @param newOfferId Internal Offer identifier - */ - public OfferDTO(final String newOfferId) { - this.offerId = newOfferId; - } + private String lastUpdate; } diff --git a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java index 7d7ea70..00956b1 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java @@ -40,6 +40,11 @@ public class ProductDTO { */ private boolean available; + /** + * Last update from API. + */ + private String lastUpdate; + /** * Creates ProductDTO instance. * diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java index ce7e63b..3403b00 100644 --- a/src/main/java/de/rwu/easydrop/core/Core.java +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -4,42 +4,74 @@ import java.util.List; import javax.naming.ConfigurationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sqlite.SQLiteDataSource; + +import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; +import de.rwu.easydrop.data.connector.SQLiteConnector; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.processing.OfferIdentifier; +import de.rwu.easydrop.service.processing.OfferProvisioner; +import de.rwu.easydrop.service.retriever.CatalogueRetriever; +import de.rwu.easydrop.service.retriever.ProductRetriever; +import de.rwu.easydrop.service.writer.CatalogueWriter; +import de.rwu.easydrop.util.Config; +import de.rwu.easydrop.util.ProductsConfig; /** * The application core. * * @since 0.3.0 */ -public class Core { +public final class Core { /** - * Offer identifier. + * Logging instance. */ - private OfferIdentifier ident; - /** - * Offer provisioner. - */ - private OfferProvisioner provis; + private static final Logger LOGGER = LoggerFactory.getLogger(Core.class); /** - * Constructor. - * - * @throws ConfigurationException + * Hidden Constructor. */ - public Core() throws ConfigurationException { - this.ident = new OfferIdentifier(); - this.provis = new OfferProvisioner(); - + private Core() { + // Hidden constructor } /** * Runs the core. * - * @param pCats + * @throws ConfigurationException */ - public void runCore(final List pCats) { + public static void run() throws ConfigurationException { + LOGGER.info("Loading config..."); + Config config = Config.getInstance(); + ProductsConfig pConfig = ProductsConfig.getInstance(); + + LOGGER.info("Preparing..."); + DataSourceFactory dataSourceFactory = new DataSourceFactory(config); + ProductPersistenceInterface pdb = new SQLiteConnector(new SQLiteDataSource()); + OfferPersistenceInterface odb = new SQLiteConnector(new SQLiteDataSource()); + ProductRetriever retriever = new ProductRetriever(dataSourceFactory, pdb); + CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); + CatalogueWriter catWriter = new CatalogueWriter(pdb); + + LOGGER.info("Loading catalogues"); + catRetriever.loadCatalogues(); + List pCats = catRetriever.getProductCatalogues(); + catWriter.writeCatalogues(pCats); + + LOGGER.info("Creating offers"); + OfferIdentifier ident = new OfferIdentifier(); List identifiedOffers = ident.runIdentifier(pCats); + OfferProvisioner provis = new OfferProvisioner(odb); provis.runProvisioner(identifiedOffers); + + // LOGGER.info("Creating transactions"); + // TODO: Transactions + + LOGGER.info("Done!"); } } diff --git a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java deleted file mode 100644 index 99d4b23..0000000 --- a/src/main/java/de/rwu/easydrop/core/OfferIdentifier.java +++ /dev/null @@ -1,83 +0,0 @@ -package de.rwu.easydrop.core; - -import javax.naming.ConfigurationException; -import java.util.List; -import java.util.ArrayList; -import java.util.Date; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import de.rwu.easydrop.model.Offer; -import de.rwu.easydrop.model.ProductCatalogue; -import de.rwu.easydrop.model.ProductPair; -import de.rwu.easydrop.service.retriever.OfferRetriever; -import de.rwu.easydrop.service.processing.OrderManager; -import de.rwu.easydrop.exception.InvalidCatalogueException; - -public class OfferIdentifier { - /** - * Logger for main process. - */ - private static final Logger LOGGER = LoggerFactory.getLogger(OfferIdentifier.class); - /** - * OfferRetriever gets the offer from persistence. - */ - private OfferRetriever offerRetriever; - - /** - * OfferIdentifier identifies offers that are - * feasible to place on a target platform based on - * their margin. - * - * @throws ConfigurationException - */ - public OfferIdentifier() throws ConfigurationException { - this.offerRetriever = new OfferRetriever(); - } - - /** - * runIdentifier calls the price function that decides - * if it is feasible to dropship products and if so, - * at which margin. - * - * @param pCats - * @return newOffers - */ - public List runIdentifier(final List pCats) { - List identifiedOffers = new ArrayList<>(); - for (ProductCatalogue pCat : pCats) { - try { - // Call price finder for all catalogue - ProductPair pair = OrderManager.getHighestMarginProducts(pCat); - Offer possibleOffer = new Offer(); - possibleOffer.setCheckDate(new Date()); - possibleOffer.setSourceProduct(pair.getProduct1()); - possibleOffer.setSaleProduct(pair.getProduct2()); - identifiedOffers.add(possibleOffer); - LOGGER.info( - "Identified offer " - + - pair.getProduct1().getProductId() - + - " -> " - + - pair.getProduct2().getProductId()); - // Following fields will be set if offer confirmed - // creationDate, offerId - // Following fields will be set if offer needs update - // upDate - } catch (InvalidCatalogueException e) { - // if no margin, getHighestMarginProducts will throw - System.out.print("product has no margin"); - } - } - List newOffers = new ArrayList<>(); - for (Offer identifiedOffer : identifiedOffers) { - boolean isNew = true; - if (isNew) { - newOffers.add(identifiedOffer); - } - } - return newOffers; - } -} diff --git a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java b/src/main/java/de/rwu/easydrop/core/OfferReviewer.java deleted file mode 100644 index e394093..0000000 --- a/src/main/java/de/rwu/easydrop/core/OfferReviewer.java +++ /dev/null @@ -1,81 +0,0 @@ -package de.rwu.easydrop.core; - -import de.rwu.easydrop.model.Offer; -import de.rwu.easydrop.model.Product; - -import java.util.List; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Date; - -public class OfferReviewer { - - /** - * Check all Offers and compare them with the API. - * @return list of all items that need to be changed - */ - public List checkOffer(/*OfferReader/retriever for database? */) { - - Connection connection = null; - Statement statement = null; - ResultSet resultSet = null; - List changedOffers = new ArrayList<>(); - - try { - // Establish the database connection - connection = DriverManager.getConnection("jdbc:sqlite:persistence.db"); - - // Create a SQL statement - statement = connection.createStatement(); - - // Execute the query to retrieve the entries - String query = "SELECT sourceProduct, saleProduct, creationDate, upDate, " - + "checkDate, offerId FROM table"; - resultSet = statement.executeQuery(query); - - // Process the retrieved entries - while (resultSet.next()) { - Product sourceProduct = (Product) resultSet.getObject("sourceProduct"); - Product saleProduct = (Product) resultSet.getObject("saleProduct"); - java.sql.Date creationDate = resultSet.getDate("creationDate"); - Date updateDate = resultSet.getDate("upDate"); - Date checkDate = resultSet.getDate("checkDate"); - String offerId = resultSet.getString("offerId"); - - // Call the API to get the current price - double apiPrice = getPriceFromAPI(sourceProduct); - - // Compare the prices - if (saleProduct.getCurrentPrice() != apiPrice) { - // Price has changed, create a Product object and add it to the changedProducts list - Offer offer = new Offer(); - changedOffers.add(offer); - } - } - } catch (SQLException e) { - e.printStackTrace(); - } finally { - try { - if (resultSet != null) { - resultSet.close(); - } - if (statement != null) { - statement.close(); - } - if (connection != null) { - connection.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - } - return new ArrayList(); - } - - - -} diff --git a/src/main/java/de/rwu/easydrop/core/OfferUpdater.java b/src/main/java/de/rwu/easydrop/core/OfferUpdater.java deleted file mode 100644 index 27d08a9..0000000 --- a/src/main/java/de/rwu/easydrop/core/OfferUpdater.java +++ /dev/null @@ -1,110 +0,0 @@ -package de.rwu.easydrop.core; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.List; -import de.rwu.easydrop.model.Offer; -import de.rwu.easydrop.model.Product; - - -public class OfferUpdater { - - /** - * a. - */ - public OfferUpdater() { - - OfferReviewer offerReviewer = new OfferReviewer(); - List changedOffers = offerReviewer.checkOffer(); - } - - /** - * A. - * @param offersToUpdate - */ - public void runUpdater(final List offersToUpdate) { - Connection connection = null; - PreparedStatement deleteStatement = null; - PreparedStatement insertStatement = null; - - try { - // Establish the database connection - connection = DriverManager.getConnection("jdbc:sqlite:persistence.db"); - - // Disable auto-commit to perform a transaction - connection.setAutoCommit(false); - - // Prepare the DELETE statement to remove the existing entries - String deleteQuery = "DELETE FROM your_table WHERE product_id = ?"; - deleteStatement = connection.prepareStatement(deleteQuery); - - // Prepare the INSERT statement to add the new entries - String insertQuery = "INSERT INTO your_table (product_id, product_name, price)" - + "VALUES (?, ?, ?)"; - insertStatement = connection.prepareStatement(insertQuery); - - // Retrieve the existing entries from the database - List existingProducts = retrieveExistingProducts(connection); - - // Delete the existing entries that are not present in the changedProducts list - for (Product existingProduct : existingProducts) { - if (!changedOffers.(existingProduct)) { - deleteStatement.setString(1, existingProduct.getProductId()); - deleteStatement.executeUpdate(); - } - } - - // Insert the new entries or update the existing entries - for (Product changedOffers : offersToUpdate) { - if (existingProducts.contains(changedOffers)) { - // Update the existing entry with the new data - // You need to modify the update query and statement based on your requirements - // Here's an example of updating the price of an existing entry - String updateQuery = "UPDATE table SET currentPrice = ? WHERE offerId = ?"; - PreparedStatement updateStatement = connection.prepareStatement(updateQuery); - updateStatement.setDouble(1, changedOffers.getCurrentPrice()); - updateStatement.setString(2, changedOffers.getProductId()); - updateStatement.executeUpdate(); - updateStatement.close(); - } else { - // Insert the new entry - insertStatement.setString(1, changedOffers.getProductId()); - insertStatement.setString(2, changedOffers.getMerchant()); - insertStatement.setDouble(3, changedOffers.getCurrentPrice()); - insertStatement.executeUpdate(); - } - } - - // Commit the transaction - connection.commit(); - } catch (SQLException e) { - // Rollback the transaction in case of an exception - if (connection != null) { - try { - connection.rollback(); - } catch (SQLException rollbackException) { - rollbackException.printStackTrace(); - } - } - e.printStackTrace(); - } finally { - // Close the resources (deleteStatement, insertStatement, connection) in a finally block - try { - if (deleteStatement != null) { - deleteStatement.close(); - } - if (insertStatement != null) { - insertStatement.close(); - } - if (connection != null) { - connection.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - } -} -} - diff --git a/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java b/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java deleted file mode 100644 index 6a302dc..0000000 --- a/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.rwu.easydrop.data.connector; - -import de.rwu.easydrop.api.client.AbstractDataSource; -import de.rwu.easydrop.api.dto.ProductDTO; - -/** - * Allows connecting to a persistent product data store. - * - * @since 0.2.0 - */ -public abstract class AbstractProductPersistence extends AbstractDataSource { - /** - * Data origin. - */ - public static final String DATA_ORIGIN = "Persistence"; - - /** - * Writes a ProductDTO to persistence. - * - * @param dto - */ - public abstract void saveProduct(ProductDTO dto); - - /** - * Gets a ProductDTO from persistence. - */ - @Override - public abstract ProductDTO getProductDTOById(String productId); - - /** - * Deletes all data from persistence. - */ - public abstract void clearData(); -} diff --git a/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java b/src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java similarity index 64% rename from src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java rename to src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java index 5f4bbeb..5d2284b 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/AbstractOfferPersistence.java +++ b/src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java @@ -2,13 +2,13 @@ package de.rwu.easydrop.data.connector; import de.rwu.easydrop.api.dto.OfferDTO; -public abstract class AbstractOfferPersistence { +public interface OfferPersistenceInterface { /** * Writes a ProductDTO to persistence. * * @param dto */ - public abstract void saveOffer(OfferDTO dto); + void writeOffer(OfferDTO dto); /** * Gets a OfferDTO from persistence. @@ -16,10 +16,10 @@ public abstract class AbstractOfferPersistence { * @param offerId * @return Offer data transfer object */ - public abstract OfferDTO getOfferDTOById(String offerId); + OfferDTO getOfferDTOById(String offerId); /** * Deletes all data from persistence. */ - public abstract void clearData(); + void clearData(); } diff --git a/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java b/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java new file mode 100644 index 0000000..b5a1c1a --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java @@ -0,0 +1,30 @@ +package de.rwu.easydrop.data.connector; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Allows connecting to a persistent product data store. + * + * @since 0.2.0 + */ +public interface ProductPersistenceInterface { + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + void writeProduct(ProductDTO dto); + + /** + * Gets a ProductDTO from persistence. + * + * @param productId Product identifier + * @return Product data transfer object + */ + ProductDTO getProductDTOById(String productId); + + /** + * Deletes all data from persistence. + */ + void clearData(); +} diff --git a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java index 43bb4fe..ba43d53 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -1,6 +1,5 @@ package de.rwu.easydrop.data.connector; -import java.net.URL; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -9,6 +8,7 @@ import java.sql.Statement; import org.sqlite.SQLiteDataSource; +import de.rwu.easydrop.api.dto.OfferDTO; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.model.Webshop; @@ -18,7 +18,8 @@ import de.rwu.easydrop.model.Webshop; * * @since 0.2.0 */ -public final class SQLiteConnector extends AbstractProductPersistence { +public final class SQLiteConnector implements + ProductPersistenceInterface, OfferPersistenceInterface { /** * SQLite Database. */ @@ -53,8 +54,8 @@ public final class SQLiteConnector extends AbstractProductPersistence { Connection connection = db.getConnection(); // Execute SQL statements to create tables - Statement statement = connection.createStatement(); - statement.execute( + Statement createProducts = connection.createStatement(); + createProducts.execute( "CREATE TABLE IF NOT EXISTS products (" + "dataOrigin TEXT, " + "productId TEXT, " @@ -62,12 +63,29 @@ public final class SQLiteConnector extends AbstractProductPersistence { + "merchant TEXT, " + "deliveryPrice REAL, " + "available INT, " - + "lastupdate TEXT, " + + "lastUpdate TEXT, " + "UNIQUE(productId, dataOrigin) ON CONFLICT REPLACE" + ")"); - // Close the statement and connection - statement.close(); + // Close the statement + createProducts.close(); + + Statement createOffers = connection.createStatement(); + createOffers.execute( + "CREATE TABLE IF NOT EXISTS offers (" + + "offerId TEXT, " + + "sourceWebshop TEXT, " + + "sourceId TEXT," + + "sourcePrice REAL, " + + "targetWebshop TEXT, " + + "targetId TEXT, " + + "targetPrice REAL, " + + "lastUpdate TEXT, " + + "UNIQUE(offerId) ON CONFLICT REPLACE" + + ")"); + createOffers.close(); + + // Close the connection connection.close(); } catch (SQLException e) { throw new PersistenceException("Something went wrong while initializing SQLite DB", e); @@ -79,12 +97,12 @@ public final class SQLiteConnector extends AbstractProductPersistence { * * @param dto */ - public void saveProduct(final ProductDTO dto) { + public void writeProduct(final ProductDTO dto) { String query = "INSERT INTO products (" + "dataOrigin, productId, currentPrice, merchant, " - + "deliveryPrice, available, lastupdate" + + "deliveryPrice, available, lastUpdate" + ") VALUES (" - + "?, ?, ?, ?, ?, ?, datetime('now', 'localtime')" + + "?, ?, ?, ?, ?, ?, ?" + ")"; try (Connection connection = db.getConnection(); @@ -97,6 +115,7 @@ public final class SQLiteConnector extends AbstractProductPersistence { statement.setString(++index, dto.getMerchant()); statement.setDouble(++index, dto.getDeliveryPrice()); statement.setBoolean(++index, dto.isAvailable()); + statement.setString(++index, dto.getLastUpdate()); statement.executeUpdate(); } catch (SQLException e) { @@ -138,7 +157,7 @@ public final class SQLiteConnector extends AbstractProductPersistence { public void clearData() { try (Connection connection = db.getConnection(); Statement statement = connection.createStatement()) { - String query = "DELETE FROM products"; + String query = "DELETE FROM products; DELETE FROM offers;"; statement.executeUpdate(query); } catch (SQLException e) { throw new PersistenceException("Something went wrong while clearing the database", e); @@ -146,26 +165,68 @@ public final class SQLiteConnector extends AbstractProductPersistence { } @Override - protected Webshop getDataOrigin() { - throw new UnsupportedOperationException( - this.getClass().getName() + " doesn't support getDataOrigin"); + public void writeOffer(final OfferDTO dto) { + String query = "INSERT INTO offers (" + + "offerId, " + + "sourceWebshop, sourceId, sourcePrice, " + + "targetWebshop, targetId, targetPrice, " + + "lastUpdate" + + ") VALUES (" + + "?, ?, ?, ?, ?, ?, ?, ?" + + ")"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + int index = 0; + ProductDTO sourceProduct = dto.getSourceProduct(); + ProductDTO targetProduct = dto.getTargetProduct(); + + statement.setString(++index, dto.getOfferId()); + statement.setString(++index, sourceProduct.getProductId()); + statement.setString(++index, sourceProduct.getDataOrigin().toString()); + statement.setDouble(++index, sourceProduct.getCurrentPrice()); + statement.setString(++index, targetProduct.getProductId()); + statement.setString(++index, targetProduct.getDataOrigin().toString()); + statement.setDouble(++index, targetProduct.getCurrentPrice()); + statement.setString(++index, dto.getLastUpdate()); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while saving to SQLite", e); + } } @Override - protected String getApiKey() { - throw new UnsupportedOperationException( - this.getClass().getName() + " doesn't support getApiKey"); - } + public OfferDTO getOfferDTOById(final String offerId) { + String query = "SELECT * FROM offers WHERE offerId = ?"; + OfferDTO dto = null; - @Override - protected ProductDTO buildProductDTO(final ProductDTO product, final String json) { - throw new UnsupportedOperationException( - this.getClass().getName() + " doesn't support buildProductDTO"); - } + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { - @Override - protected URL createApiUrl(final String productIdentifier) { - throw new UnsupportedOperationException( - this.getClass().getName() + " doesn't support createApiUrl"); + statement.setString(1, offerId); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + dto = new OfferDTO(); + ProductDTO srcProduct = new ProductDTO( + resultSet.getString("sourceId"), + Webshop.fromString(resultSet.getString("sourceWebshop"))); + srcProduct.setCurrentPrice(resultSet.getDouble("sourcePrice")); + ProductDTO targetProduct = new ProductDTO( + resultSet.getString("targetId"), + Webshop.fromString(resultSet.getString("targetWebshop"))); + srcProduct.setCurrentPrice(resultSet.getDouble("targetPrice")); + dto.setOfferId(resultSet.getString("offerId")); + dto.setSourceProduct(srcProduct); + dto.setTargetProduct(targetProduct); + dto.setLastUpdate(resultSet.getString("lastUpdate")); + } + } + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while reading from SQLite", e); + } + + return dto; } } diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java b/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java new file mode 100644 index 0000000..f4d2c0b --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java @@ -0,0 +1,22 @@ +package de.rwu.easydrop.exception; + +public class InvalidOfferException extends RuntimeException { + /** + * Throws an exception that signifies the data of an Offer are invalid. + * + * @param message + */ + public InvalidOfferException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of an Offer are invalid. + * + * @param message + * @param cause + */ + public InvalidOfferException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/model/Offer.java b/src/main/java/de/rwu/easydrop/model/Offer.java index 8fae3b7..b2e3577 100644 --- a/src/main/java/de/rwu/easydrop/model/Offer.java +++ b/src/main/java/de/rwu/easydrop/model/Offer.java @@ -1,8 +1,5 @@ package de.rwu.easydrop.model; -import java.util.Date; - - import lombok.Data; /** @@ -10,9 +7,13 @@ import lombok.Data; * * @since 0.3.0 */ - @Data public class Offer { + /** + * ID of the offer, built from identifiers of the source platforms. + */ + private String offerId; + /** * The product that our software buys. */ @@ -21,27 +22,10 @@ public class Offer { /** * The product that our software sells. */ - private Product saleProduct; + private Product targetProduct; /** - * Date of the creation of the offer. - * NOTE: Use Timestamp? https://docs.oracle.com/javase/8/docs/api/java/sql/Timestamp.html + * Date of last update of the offer. */ - private Date creationDate; - - /** - * Date of last update of the offer on the destination website (API). - */ - private Date upDate; - - /** - * Date of last check if offer is still valid. - */ - private Date checkDate; - - /** - * ID of the offer. - */ - private String offerId; - + private String lastUpdate; } diff --git a/src/main/java/de/rwu/easydrop/model/Product.java b/src/main/java/de/rwu/easydrop/model/Product.java index 7e84311..dd412dd 100644 --- a/src/main/java/de/rwu/easydrop/model/Product.java +++ b/src/main/java/de/rwu/easydrop/model/Product.java @@ -40,6 +40,11 @@ public class Product { */ private boolean available; + /** + * Last update from API. + */ + private String lastUpdate; + @Override public final String toString() { return "Product: [" diff --git a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java index 97f31dc..2e91d1c 100644 --- a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java +++ b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java @@ -68,7 +68,7 @@ public class ProductCatalogue { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("Product Catalogue: ").append(productName).append("\n"); + sb.append("Catalogue Name: ").append(productName).append("\n"); sb.append("Description: ").append(description).append("\n"); sb.append("Products:\n"); for (Product product : products) { diff --git a/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java index a83a148..aa98afe 100644 --- a/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java +++ b/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java @@ -1,7 +1,6 @@ package de.rwu.easydrop.service.mapping; import de.rwu.easydrop.api.dto.OfferDTO; - import de.rwu.easydrop.model.Offer; /** @@ -31,6 +30,10 @@ public final class OfferMapper { */ public static Offer mapOfferFromDTO(final OfferDTO dto) { Offer offer = new Offer(); + offer.setOfferId(dto.getOfferId()); + offer.setSourceProduct(ProductMapper.mapProductFromDTO(dto.getSourceProduct())); + offer.setTargetProduct(ProductMapper.mapProductFromDTO(dto.getTargetProduct())); + offer.setLastUpdate(dto.getLastUpdate()); return offer; } @@ -42,7 +45,11 @@ public final class OfferMapper { * @return OfferDTO */ public static OfferDTO mapOfferToDTO(final Offer offer) { - OfferDTO dto = new OfferDTO(offer.getOfferId()); + OfferDTO dto = new OfferDTO(); + dto.setOfferId(offer.getOfferId()); + dto.setSourceProduct(ProductMapper.mapProductToDTO(offer.getSourceProduct())); + dto.setTargetProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); + dto.setLastUpdate(offer.getLastUpdate()); return dto; } diff --git a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java index 123080a..6ada1b4 100644 --- a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java +++ b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java @@ -39,6 +39,7 @@ public final class ProductMapper { product.setDeliveryPrice(dto.getDeliveryPrice()); product.setMerchant(dto.getMerchant()); product.setProductId(dto.getProductId()); + product.setLastUpdate(dto.getLastUpdate()); return product; } @@ -56,6 +57,7 @@ public final class ProductMapper { dto.setCurrentPrice(product.getCurrentPrice()); dto.setDeliveryPrice(product.getDeliveryPrice()); dto.setMerchant(product.getMerchant()); + dto.setLastUpdate(product.getLastUpdate()); return dto; } diff --git a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java b/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java similarity index 64% rename from src/main/java/de/rwu/easydrop/service/processing/OrderManager.java rename to src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java index d841505..629d594 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java @@ -1,56 +1,56 @@ package de.rwu.easydrop.service.processing; +import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.rwu.easydrop.exception.InvalidCatalogueException; +import de.rwu.easydrop.exception.InvalidOfferException; +import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.model.ProductPair; import de.rwu.easydrop.util.FormattingUtil; +import de.rwu.easydrop.util.Timestamp; -/** - * Creates dropshipping orders based on price margin. - * - * @since 0.3.0 - */ -public final class OrderManager { - +public class OfferIdentifier { /** - * Temporary logging instance. + * Logging instance. */ - private static final Logger LOGGER = LoggerFactory.getLogger(OrderManager.class); + private static final Logger LOGGER = LoggerFactory.getLogger(OfferIdentifier.class); /** - * Private constructor to prevent unwanted instantiation. + * runIdentifier calls the price function that decides + * if it is feasible to dropship products. * - * @throws UnsupportedOperationException always + * @param pCats Product catalogues + * @return Identified offers */ - private OrderManager() throws UnsupportedOperationException { - throw new UnsupportedOperationException("This is a stateless class, don't instantiate it."); - } - - /** - * Creates orders for products with sufficient margin. - * - * @param pCats Product Catalogues - */ - public static void createOrders(final List pCats) { + public List runIdentifier(final List pCats) { + List identifiedOffers = new ArrayList<>(); for (ProductCatalogue pCat : pCats) { - ProductPair pair = getHighestMarginProducts(pCat); - // #12: Create actual orders/transactions, remove logger + // Call price finder for all catalogue + ProductPair pair = getHighestMarginProducts(pCat); + Offer possibleOffer = new Offer(); + possibleOffer.setLastUpdate(Timestamp.now()); + possibleOffer.setSourceProduct(pair.getProduct1()); + possibleOffer.setTargetProduct(pair.getProduct2()); + identifiedOffers.add(possibleOffer); + double margin = pair.getProduct2().getCurrentPrice() - pair.getProduct1().getCurrentPrice(); String marginFormatted = FormattingUtil.formatEuro(margin); - LOGGER.info("{}: Margin {} ({} to {})", + LOGGER.info("\n Identified Offer: {} ({} to {}) with margin {} ", pCat.getProductName(), - marginFormatted, pair.getProduct1().getDataOrigin(), - pair.getProduct2().getDataOrigin()); + pair.getProduct2().getDataOrigin(), + marginFormatted); } + + return identifiedOffers; } /** @@ -79,7 +79,7 @@ public final class OrderManager { } if (cheapestProduct.getCurrentPrice() == mostExpensiveProduct.getCurrentPrice()) { - throw new InvalidCatalogueException("Price margin is zero!"); + throw new InvalidOfferException("Price margin is zero!"); } return new ProductPair(cheapestProduct, mostExpensiveProduct); diff --git a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java similarity index 57% rename from src/main/java/de/rwu/easydrop/core/OfferProvisioner.java rename to src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java index 0f318bd..d6801c7 100644 --- a/src/main/java/de/rwu/easydrop/core/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java @@ -1,15 +1,16 @@ -package de.rwu.easydrop.core; +package de.rwu.easydrop.service.processing; import java.util.List; import de.rwu.easydrop.api.client.AmazonSeller; import de.rwu.easydrop.api.client.EbaySeller; -import de.rwu.easydrop.exception.DataWriterException; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.writer.OfferWriter; import de.rwu.easydrop.util.Config; +import de.rwu.easydrop.util.FormattingUtil; public class OfferProvisioner { /** @@ -31,11 +32,11 @@ public class OfferProvisioner { private EbaySeller ebaySeller; private void toSeller(final Offer offer) throws IllegalArgumentException { - if (offer.getSaleProduct().getDataOrigin() == Webshop.eBay) { - this.ebaySeller.sellProduct(ProductMapper.mapProductToDTO(offer.getSaleProduct())); + if (offer.getTargetProduct().getDataOrigin() == Webshop.eBay) { + this.ebaySeller.sellProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); - } else if (offer.getSaleProduct().getDataOrigin().equals(Webshop.Amazon)) { - this.amazonSeller.sellProduct(ProductMapper.mapProductToDTO(offer.getSaleProduct())); + } else if (offer.getTargetProduct().getDataOrigin().equals(Webshop.Amazon)) { + this.amazonSeller.sellProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); } else { throw new IllegalArgumentException("Unsupported target plattform"); } @@ -44,10 +45,11 @@ public class OfferProvisioner { /** * Is the class for placing orders on a target platform. + * + * @param db Persistence Interface */ - public OfferProvisioner(/* OfferWriter for database? */) { - - this.offerWriter = new OfferWriter(); + public OfferProvisioner(final OfferPersistenceInterface db) { + this.offerWriter = new OfferWriter(db); this.config = Config.getInstance(); this.amazonSeller = new AmazonSeller( config.getProperty("AMAZON_API_URL"), @@ -63,27 +65,17 @@ public class OfferProvisioner { * @param offersToProvision */ public final void runProvisioner(final List offersToProvision) { - for (Offer newOffer : offersToProvision) { + String newOfferId = FormattingUtil.removeSpaces( + newOffer.getSourceProduct().getDataOrigin().toString() + + newOffer.getTargetProduct().getDataOrigin().toString() + + "_" + + newOffer.getSourceProduct().getProductId() + + newOffer.getTargetProduct().getProductId()); - try { - this.toSeller(newOffer); - // if successfully transmitted - // add to persistence - // "duplicate" the product with dataOrigin new platform and merchant = "me" - try { - offerWriter.writeOfferToPersistence(newOffer); - } catch (Exception e) { - System.out.println("Could not write to persistence"); - } - } catch (IllegalArgumentException e) { - System.out.println( - "Offer could not be placed, " - + newOffer.getSaleProduct().getDataOrigin() - + " is not supported"); - } catch (DataWriterException e) { - System.out.println("could not transmit offer"); - } + this.toSeller(newOffer); + newOffer.setOfferId(newOfferId); + offerWriter.writeOfferToPersistence(newOffer); } } } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java index c299412..9af6436 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java @@ -5,6 +5,9 @@ import java.util.List; import javax.naming.ConfigurationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.util.ProductsConfig; @@ -17,6 +20,11 @@ import lombok.Data; */ @Data public class CatalogueRetriever { + /** + * Logging instance. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(CatalogueRetriever.class); + /** * User-configured products. */ @@ -56,15 +64,14 @@ public class CatalogueRetriever { pCat.getProductName(), pCat.getDescription()); for (Product product : pCat.getProducts()) { - Product newProduct = new Product(); - - newProduct = productRetriever.getProductFromWebshop(product.getDataOrigin(), + Product newProduct = productRetriever.getProductFromWebshop(product.getDataOrigin(), product.getProductId()); newProductCatalogue.addProduct(newProduct); } productCatalogues.add(newProductCatalogue); + LOGGER.info("\nLoaded Catalogue: \n" + newProductCatalogue.toString()); } } } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java index 5488b29..4cef58d 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java @@ -1,5 +1,40 @@ package de.rwu.easydrop.service.retriever; -public class OfferRetriever { +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.service.mapping.OfferMapper; +/** + * Retrieves offer information from different sources. + * + * @since 0.3.0 + */ +public class OfferRetriever { + /** + * Persistence interface. + */ + private OfferPersistenceInterface persistence; + + /** + * Creates an Offer Retriever. + * + * @param db Persistence Interface + */ + public OfferRetriever(final OfferPersistenceInterface db) { + this.persistence = db; + } + + /** + * Retrieves an offer from persistence. + * + * @param offerId + * @return Offer from persistence + */ + public Offer getOfferFromPersistence(final String offerId) { + OfferPersistenceInterface src = persistence; + + OfferDTO dto = src.getOfferDTOById(offerId); + return OfferMapper.mapOfferFromDTO(dto); + } } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java index ddbe4b1..cd5cb30 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -4,7 +4,7 @@ import de.rwu.easydrop.api.client.AmazonProductDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.ProductDTO; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.service.mapping.ProductMapper; @@ -20,19 +20,20 @@ public class ProductRetriever { * Data source factory. */ private DataSourceFactory dataSourceFactory; - /** - * @param newDataSourceFactory the WebshopFactory to set + * Persistence interface. */ - public void setWebshopFactory(final DataSourceFactory newDataSourceFactory) { - this.dataSourceFactory = newDataSourceFactory; - } + private ProductPersistenceInterface persistence; /** * @param newDataSourceFactory + * @param newPersistence */ - public ProductRetriever(final DataSourceFactory newDataSourceFactory) { - this.setWebshopFactory(newDataSourceFactory); + public ProductRetriever( + final DataSourceFactory newDataSourceFactory, + final ProductPersistenceInterface newPersistence) { + this.dataSourceFactory = newDataSourceFactory; + this.persistence = newPersistence; } /** @@ -74,9 +75,7 @@ public class ProductRetriever { * @return Product from persistence */ public Product getProductFromPersistence(final String productId) { - AbstractProductPersistence src = dataSourceFactory.createProductPersistenceDataSource(); - - ProductDTO dto = src.getProductDTOById(productId); + ProductDTO dto = persistence.getProductDTOById(productId); Product product = ProductMapper.mapProductFromDTO(dto); ProductValidator.validate(product); diff --git a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java index c3f2257..82fbd89 100644 --- a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java +++ b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java @@ -1,5 +1,6 @@ package de.rwu.easydrop.service.validation; +import de.rwu.easydrop.exception.InvalidOfferException; import de.rwu.easydrop.model.Offer; /** @@ -8,7 +9,7 @@ import de.rwu.easydrop.model.Offer; * @since 0.2.0 */ public final class OfferValidator { - /** + /** * Private constructor to prevent unwanted instantiation. * * @throws UnsupportedOperationException always @@ -16,13 +17,19 @@ public final class OfferValidator { private OfferValidator() throws UnsupportedOperationException { throw new UnsupportedOperationException("This is a validator class, don't instantiate it."); } - /** + + /** * Makes sure an Offer does not contain invalid information. * * @param offer the Offer */ - public static void validate(final Offer offer) { } + public static void validate(final Offer offer) { + try { + if (offer.getOfferId().equals("")) { + throw new InvalidOfferException("Offer ID cannot be empty"); + } + } catch (NullPointerException e) { + throw new InvalidOfferException("Required information is missing in the offer", e); + } + } } - - - diff --git a/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java b/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java index a1cf486..c243f60 100644 --- a/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java +++ b/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java @@ -3,7 +3,7 @@ package de.rwu.easydrop.service.writer; import java.util.List; import de.rwu.easydrop.api.dto.ProductDTO; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.service.mapping.ProductMapper; @@ -18,14 +18,14 @@ public final class CatalogueWriter { /** * Holds a persistence reference. */ - private AbstractProductPersistence persistence; + private ProductPersistenceInterface persistence; /** * Creates new instance. * * @param newPersistence */ - public CatalogueWriter(final AbstractProductPersistence newPersistence) { + public CatalogueWriter(final ProductPersistenceInterface newPersistence) { persistence = newPersistence; } @@ -40,7 +40,7 @@ public final class CatalogueWriter { ProductValidator.validate(product); ProductDTO dto = ProductMapper.mapProductToDTO(product); - persistence.saveProduct(dto); + persistence.writeProduct(dto); } } } diff --git a/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java b/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java index 6ba6a4c..ba43912 100644 --- a/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java +++ b/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java @@ -1,21 +1,20 @@ package de.rwu.easydrop.service.writer; import de.rwu.easydrop.api.dto.OfferDTO; -import de.rwu.easydrop.data.connector.AbstractOfferPersistence; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.service.mapping.OfferMapper; -import de.rwu.easydrop.service.validation.OfferValidator; public class OfferWriter { /** * Persistence. */ - private AbstractOfferPersistence persistence; + private OfferPersistenceInterface persistence; /** * @param newPersistence the persistence to set */ - public void setPersistence(final AbstractOfferPersistence newPersistence) { + public OfferWriter(final OfferPersistenceInterface newPersistence) { this.persistence = newPersistence; } @@ -25,9 +24,8 @@ public class OfferWriter { * @param offer */ public void writeOfferToPersistence(final Offer offer) { - OfferValidator.validate(offer); OfferDTO dto = OfferMapper.mapOfferToDTO(offer); - persistence.saveOffer(dto); + persistence.writeOffer(dto); } } diff --git a/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java b/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java index d1be850..79d8114 100644 --- a/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java +++ b/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java @@ -1,7 +1,7 @@ package de.rwu.easydrop.service.writer; import de.rwu.easydrop.api.dto.ProductDTO; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.validation.ProductValidator; @@ -15,12 +15,12 @@ public class ProductWriter { /** * Persistence. */ - private AbstractProductPersistence persistence; + private ProductPersistenceInterface persistence; /** * @param newPersistence the persistence to set */ - public void setPersistence(final AbstractProductPersistence newPersistence) { + public ProductWriter(final ProductPersistenceInterface newPersistence) { this.persistence = newPersistence; } @@ -33,6 +33,6 @@ public class ProductWriter { ProductValidator.validate(product); ProductDTO dto = ProductMapper.mapProductToDTO(product); - persistence.saveProduct(dto); + persistence.writeProduct(dto); } } diff --git a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java index e8ea662..544e729 100644 --- a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java +++ b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java @@ -37,4 +37,14 @@ public final class FormattingUtil { public static String urlEncode(final String str) { return str.replace(" ", "+"); } + + /** + * Removes spaces from target string. + * + * @param str String + * @return Space-less string + */ + public static String removeSpaces(final String str) { + return str.replace(" ", ""); + } } diff --git a/src/main/java/de/rwu/easydrop/util/Timestamp.java b/src/main/java/de/rwu/easydrop/util/Timestamp.java new file mode 100644 index 0000000..a087982 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/util/Timestamp.java @@ -0,0 +1,29 @@ +package de.rwu.easydrop.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Serves as a timestamp source. + * + * @since 0.3.0 + */ +public abstract class Timestamp { + /** + * Hidden constructor. + */ + private Timestamp() { + // Don't instantiate me! + } + + /** + * Returns a formatted time string. + * + * @return String such as "2023-01-01 00:00:00" + */ + public static String now() { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return now.format(formatter); + } +} From 9927b8f95936aaccf5f44de4298f97bba0b4d61f Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 27 Jun 2023 06:45:00 +0200 Subject: [PATCH 42/71] SonarLint fixes --- config/demo.products-config.json | 4 +- .../api/client/AmazonProductDataSource.java | 2 +- .../easydrop/api/client/AmazonPurchaser.java | 2 +- .../rwu/easydrop/api/client/AmazonSeller.java | 2 +- .../api/client/EbayItemDataSource.java | 2 +- .../easydrop/api/client/EbayPurchaser.java | 2 +- .../rwu/easydrop/api/client/EbaySeller.java | 2 +- src/main/java/de/rwu/easydrop/core/Core.java | 4 +- .../data/connector/DatabaseConnector.java | 10 ----- .../data/connector/SQLiteConnector.java | 45 ++++++++++++++----- .../rwu/easydrop/model/ProductCatalogue.java | 6 +-- .../java/de/rwu/easydrop/model/Webshop.java | 6 +-- .../service/processing/OfferProvisioner.java | 4 +- .../service/retriever/CatalogueRetriever.java | 4 +- .../service/retriever/ProductRetriever.java | 4 +- .../java/de/rwu/easydrop/util/Timestamp.java | 2 +- 16 files changed, 59 insertions(+), 42 deletions(-) delete mode 100644 src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java diff --git a/config/demo.products-config.json b/config/demo.products-config.json index 2aa9058..84a4f91 100644 --- a/config/demo.products-config.json +++ b/config/demo.products-config.json @@ -5,10 +5,10 @@ "description": "Very epic GPU", "identifiers": [ { - "Amazon": "B096Y2TYKV" + "AMAZON": "B096Y2TYKV" }, { - "eBay": "Gigabyte GeForce RTX 3060" + "EBAY": "Gigabyte GeForce RTX 3060" } ] } diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java index 06c806e..64d40ee 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java @@ -20,7 +20,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { /** * Name of this data source. */ - private static final Webshop DATA_ORIGIN = Webshop.Amazon; + private static final Webshop DATA_ORIGIN = Webshop.AMAZON; /** * Base URL to the Amazon data source. */ diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java index d1dc75c..af96ede 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java @@ -14,7 +14,7 @@ public final class AmazonPurchaser extends AbstractPurchaser { /** * Name of this data source. */ - private static final Webshop DATA_TARGET = Webshop.Amazon; + private static final Webshop DATA_TARGET = Webshop.AMAZON; /** * Base URL to the Amazon Purchase API. */ diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java b/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java index 7b33138..42ab6c6 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java @@ -14,7 +14,7 @@ public final class AmazonSeller extends AbstractSeller { /** * Name of this data source. */ - private static final Webshop DATA_TARGET = Webshop.Amazon; + private static final Webshop DATA_TARGET = Webshop.AMAZON; /** * Base URL to the Amazon Purchase API. */ diff --git a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java index 0b1ed40..f7a205a 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java @@ -20,7 +20,7 @@ public final class EbayItemDataSource extends AbstractDataSource { /** * Name of this data source. */ - private static final Webshop DATA_ORIGIN = Webshop.eBay; + private static final Webshop DATA_ORIGIN = Webshop.EBAY; /** * Base URL to the eBay data source. */ diff --git a/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java index ebaf121..67fc08e 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java @@ -14,7 +14,7 @@ public final class EbayPurchaser extends AbstractPurchaser { /** * Name of this data source. */ - private static final Webshop DATA_TARGET = Webshop.eBay; + private static final Webshop DATA_TARGET = Webshop.EBAY; /** * Base URL to the eBay Purchase API. */ diff --git a/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java b/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java index 55835ec..c09a852 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java @@ -14,7 +14,7 @@ public final class EbaySeller extends AbstractSeller { /** * Name of this data source. */ - private static final Webshop DATA_TARGET = Webshop.eBay; + private static final Webshop DATA_TARGET = Webshop.EBAY; /** * Base URL to the eBay Purchase API. */ diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java index 3403b00..a33c14e 100644 --- a/src/main/java/de/rwu/easydrop/core/Core.java +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -69,8 +69,8 @@ public final class Core { OfferProvisioner provis = new OfferProvisioner(odb); provis.runProvisioner(identifiedOffers); - // LOGGER.info("Creating transactions"); - // TODO: Transactions + LOGGER.info("Creating transactions"); + // Transaction logic! LOGGER.info("Done!"); } diff --git a/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java b/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java deleted file mode 100644 index ce6360a..0000000 --- a/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java +++ /dev/null @@ -1,10 +0,0 @@ -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/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java index ba43d53..4a9e492 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -36,6 +36,10 @@ public final class SQLiteConnector implements * Path to SQLite db file. */ private static final String PERSISTENCE_PATH = "jdbc:sqlite:persistence.db"; + /** + * Name of 'lastUpdate' column. + */ + private static final String LAST_UPDATE_COL_NAME = "lastUpdate"; /** * Creates instance. @@ -123,13 +127,19 @@ public final class SQLiteConnector implements } } - @Override + /** + * Gets a ProductDTO by identifier. + * + * @param productId + * @return DTO + */ public ProductDTO getProductDTOById(final String productId) { String query = "SELECT * FROM products WHERE productId = ?"; ProductDTO dto = null; - try (Connection connection = db.getConnection(); - PreparedStatement statement = connection.prepareStatement(query)) { + try { + Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query); statement.setString(1, productId); @@ -142,10 +152,11 @@ public final class SQLiteConnector implements dto.setMerchant(resultSet.getString("merchant")); dto.setDeliveryPrice(resultSet.getDouble("deliveryPrice")); dto.setAvailable(resultSet.getBoolean("available")); + dto.setLastUpdate(resultSet.getString(LAST_UPDATE_COL_NAME)); } } } catch (SQLException e) { - throw new PersistenceException("Something went wrong while reading from SQLite", e); + throw new PersistenceException("Something went wrong while reading from SQLite"); } return dto; @@ -153,24 +164,33 @@ public final class SQLiteConnector implements /** * Deletes all data from persistence. + * + * @throws SQLException */ public void clearData() { try (Connection connection = db.getConnection(); Statement statement = connection.createStatement()) { - String query = "DELETE FROM products; DELETE FROM offers;"; - statement.executeUpdate(query); + + String productsQuery = "DELETE FROM products;"; + String offersQuery = "DELETE FROM offers;"; + statement.executeUpdate(productsQuery); + statement.executeUpdate(offersQuery); } catch (SQLException e) { throw new PersistenceException("Something went wrong while clearing the database", e); } } - @Override + /** + * Writes an offer to persistence. + * + * @param dto OfferDTO + */ public void writeOffer(final OfferDTO dto) { String query = "INSERT INTO offers (" + "offerId, " + "sourceWebshop, sourceId, sourcePrice, " + "targetWebshop, targetId, targetPrice, " - + "lastUpdate" + + LAST_UPDATE_COL_NAME + ") VALUES (" + "?, ?, ?, ?, ?, ?, ?, ?" + ")"; @@ -196,7 +216,12 @@ public final class SQLiteConnector implements } } - @Override + /** + * Gets an OfferDTO by identifier. + * + * @param offerId + * @return OfferDTO + */ public OfferDTO getOfferDTOById(final String offerId) { String query = "SELECT * FROM offers WHERE offerId = ?"; OfferDTO dto = null; @@ -220,7 +245,7 @@ public final class SQLiteConnector implements dto.setOfferId(resultSet.getString("offerId")); dto.setSourceProduct(srcProduct); dto.setTargetProduct(targetProduct); - dto.setLastUpdate(resultSet.getString("lastUpdate")); + dto.setLastUpdate(resultSet.getString(LAST_UPDATE_COL_NAME)); } } } catch (SQLException e) { diff --git a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java index 2e91d1c..e907bcc 100644 --- a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java +++ b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java @@ -68,11 +68,11 @@ public class ProductCatalogue { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("Catalogue Name: ").append(productName).append("\n"); - sb.append("Description: ").append(description).append("\n"); + sb.append(String.format("Catalogue Name: %s%n", productName)); + sb.append(String.format("Description: %s%n", description)); sb.append("Products:\n"); for (Product product : products) { - sb.append(product.toString()).append("\n"); + sb.append(String.format("%s%n", product.toString())); } return sb.toString(); } diff --git a/src/main/java/de/rwu/easydrop/model/Webshop.java b/src/main/java/de/rwu/easydrop/model/Webshop.java index c4852b6..bab281e 100644 --- a/src/main/java/de/rwu/easydrop/model/Webshop.java +++ b/src/main/java/de/rwu/easydrop/model/Webshop.java @@ -7,11 +7,11 @@ public enum Webshop { /** * Amazon Product API. */ - Amazon, + AMAZON, /** * eBay Item API. */ - eBay; + EBAY; /** * Attempts to derive a webshop value from a string. @@ -26,6 +26,6 @@ public enum Webshop { return shop; } } - throw new IllegalArgumentException(String.format("No webshop called {} found", str)); + throw new IllegalArgumentException(String.format("No webshop called %s found", str)); } } diff --git a/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java index d6801c7..5c1847e 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java @@ -32,10 +32,10 @@ public class OfferProvisioner { private EbaySeller ebaySeller; private void toSeller(final Offer offer) throws IllegalArgumentException { - if (offer.getTargetProduct().getDataOrigin() == Webshop.eBay) { + if (offer.getTargetProduct().getDataOrigin() == Webshop.EBAY) { this.ebaySeller.sellProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); - } else if (offer.getTargetProduct().getDataOrigin().equals(Webshop.Amazon)) { + } else if (offer.getTargetProduct().getDataOrigin().equals(Webshop.AMAZON)) { this.amazonSeller.sellProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); } else { throw new IllegalArgumentException("Unsupported target plattform"); diff --git a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java index 9af6436..44c32a3 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java @@ -71,7 +71,9 @@ public class CatalogueRetriever { } productCatalogues.add(newProductCatalogue); - LOGGER.info("\nLoaded Catalogue: \n" + newProductCatalogue.toString()); + String catString = String.format( + "%nLoaded Catalogue: %n%s", newProductCatalogue.toString()); + LOGGER.info(catString); } } } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java index cd5cb30..8e42ea3 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -94,9 +94,9 @@ public class ProductRetriever { */ public Product getProductFromWebshop(final Webshop shop, final String productIdentifier) { switch (shop) { - case Amazon: + case AMAZON: return getProductFromAmazon(productIdentifier); - case eBay: + case EBAY: return getProductFromEbay(productIdentifier); default: return null; diff --git a/src/main/java/de/rwu/easydrop/util/Timestamp.java b/src/main/java/de/rwu/easydrop/util/Timestamp.java index a087982..1532c96 100644 --- a/src/main/java/de/rwu/easydrop/util/Timestamp.java +++ b/src/main/java/de/rwu/easydrop/util/Timestamp.java @@ -8,7 +8,7 @@ import java.time.format.DateTimeFormatter; * * @since 0.3.0 */ -public abstract class Timestamp { +public final class Timestamp { /** * Hidden constructor. */ From f2993a19a888c24aa68c0ad2b0417317bc12047a Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 27 Jun 2023 06:45:25 +0200 Subject: [PATCH 43/71] Updated test cases --- .../api/client/AbstractDataWriterTest.java | 14 +- .../api/client/AbstractPurchaserTest.java | 4 +- .../api/client/AbstractSellerTest.java | 4 +- .../client/AmazonProductDataSourceTest.java | 8 +- .../api/client/AmazonPurchaserTest.java | 2 +- .../easydrop/api/client/AmazonSellerTest.java | 2 +- .../api/client/DataSourceFactoryTest.java | 21 --- .../api/client/EbayItemDataSourceTest.java | 6 +- .../api/client/EbayPurchaserTest.java | 2 +- .../easydrop/api/client/EbaySellerTest.java | 2 +- .../rwu/easydrop/api/dto/ProductDTOTest.java | 10 +- .../easydrop/core/OfferIdentifierTest.java | 10 -- .../easydrop/core/OfferProvisionerTest.java | 10 -- .../rwu/easydrop/core/OfferReviewerTest.java | 10 -- .../rwu/easydrop/core/OfferUpdaterTest.java | 10 -- .../data/connector/SQLiteConnectorTest.java | 65 ++------- .../easydrop/model/ProductCatalogueTest.java | 29 ++-- .../de/rwu/easydrop/model/ProductTest.java | 16 +-- .../service/mapping/ProductMapperTest.java | 6 +- .../service/processing/OrderManagerTest.java | 130 ------------------ .../retriever/CatalogueRetrieverTest.java | 12 +- .../retriever/ProductRetrieverTest.java | 21 ++- .../validation/ProductValidatorTest.java | 6 +- .../service/writer/CatalogueWriterTest.java | 14 +- .../service/writer/ProductWriterTest.java | 13 +- .../test.malformed.products-config.json | 4 +- src/test/resources/test.products-config.json | 4 +- 27 files changed, 104 insertions(+), 331 deletions(-) delete mode 100644 src/test/java/de/rwu/easydrop/core/OfferIdentifierTest.java delete mode 100644 src/test/java/de/rwu/easydrop/core/OfferProvisionerTest.java delete mode 100644 src/test/java/de/rwu/easydrop/core/OfferReviewerTest.java delete mode 100644 src/test/java/de/rwu/easydrop/core/OfferUpdaterTest.java delete mode 100644 src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java index 97b007e..78c610f 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java @@ -44,7 +44,7 @@ class AbstractDataWriterTest { @Override protected Webshop getDataTarget() { - return Webshop.Amazon; + return Webshop.AMAZON; } @Override @@ -57,7 +57,7 @@ class AbstractDataWriterTest { @Test void sendPutRequest_badResponseCode_throwsException() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO(demoProductId, Webshop.Amazon); + ProductDTO dto = new ProductDTO(demoProductId, Webshop.AMAZON); // Set up Mocks AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); @@ -69,13 +69,13 @@ class AbstractDataWriterTest { writer.sendPutRequest(dto, "Sales"); }); - assertEquals("Amazon Sales API responded with error code 400", e.getMessage()); + assertEquals("AMAZON Sales API responded with error code 400", e.getMessage()); } @Test void sendPutRequest_ioException_throwsException() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO(demoProductId, Webshop.Amazon); + ProductDTO dto = new ProductDTO(demoProductId, Webshop.AMAZON); // Set up Mocks AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); @@ -86,13 +86,13 @@ class AbstractDataWriterTest { writer.sendPutRequest(dto, "testApiType"); }); - assertEquals("Couldn't fulfill Amazon API request", e.getMessage()); + assertEquals("Couldn't fulfill AMAZON API request", e.getMessage()); } @Test void sendPutRequest_successfulRequest() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO(demoProductId, Webshop.Amazon); + ProductDTO dto = new ProductDTO(demoProductId, Webshop.AMAZON); // Set up Mocks AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); @@ -102,7 +102,7 @@ class AbstractDataWriterTest { HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(mockWriter.getDataTarget()).thenReturn(Webshop.Amazon); + when(mockWriter.getDataTarget()).thenReturn(Webshop.AMAZON); assertDoesNotThrow(() -> { mockWriter.sendPutRequest(dto, "Purchase"); diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java index 0a62b8e..3bc169a 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java @@ -21,7 +21,7 @@ class AbstractPurchaserTest { @Test void purchaseProduct_CorrectApiTypeInException() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO("12345", Webshop.Amazon); + ProductDTO dto = new ProductDTO("12345", Webshop.AMAZON); // Set up mocks URL mockURL = mock(URL.class); @@ -30,7 +30,7 @@ class AbstractPurchaserTest { HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(mockPurchaser.getDataTarget()).thenReturn(Webshop.Amazon); + when(mockPurchaser.getDataTarget()).thenReturn(Webshop.AMAZON); assertDoesNotThrow(() -> { mockPurchaser.purchaseProduct(dto); diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java index e907a37..aa3c299 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java @@ -21,7 +21,7 @@ class AbstractSellerTest { @Test void purchaseProduct_CorrectApiTypeInException() throws IOException { // Set up DTO - ProductDTO dto = new ProductDTO("12345", Webshop.Amazon); + ProductDTO dto = new ProductDTO("12345", Webshop.AMAZON); // Set up mocks URL mockURL = mock(URL.class); @@ -30,7 +30,7 @@ class AbstractSellerTest { HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(mockSeller.getDataTarget()).thenReturn(Webshop.Amazon); + when(mockSeller.getDataTarget()).thenReturn(Webshop.AMAZON); assertDoesNotThrow(() -> { mockSeller.sellProduct(dto); diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java index 7735bd9..ba3bfa9 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java @@ -29,7 +29,7 @@ class AmazonProductDataSourceTest { private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static Webshop demoDataOrigin = Webshop.Amazon; + private static Webshop demoDataOrigin = Webshop.AMAZON; private static String demoProductId = "whateverId"; @BeforeEach @@ -139,7 +139,7 @@ class AmazonProductDataSourceTest { // Verify the product DTO properties assertEquals(demoProductId, result.getProductId()); - assertEquals(Webshop.Amazon, result.getDataOrigin()); + assertEquals(Webshop.AMAZON, result.getDataOrigin()); assertEquals(true, result.isAvailable()); assertEquals(10.0, result.getCurrentPrice()); assertEquals(2.5, result.getDeliveryPrice()); @@ -165,7 +165,7 @@ class AmazonProductDataSourceTest { }); // Verify the exception message - assertEquals("Nothing found: Amazon API responded with error code 404", exception.getMessage()); + assertEquals("Nothing found: AMAZON API responded with error code 404", exception.getMessage()); } @Test @@ -188,7 +188,7 @@ class AmazonProductDataSourceTest { }); // Verify the exception message - assertEquals("Couldn't fulfill Amazon API request", exception.getMessage()); + assertEquals("Couldn't fulfill AMAZON API request", exception.getMessage()); } @Test diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java index 1386e97..788fe5e 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java @@ -18,7 +18,7 @@ class AmazonPurchaserTest { private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static Webshop demoDataTarget = Webshop.Amazon; + private static Webshop demoDataTarget = Webshop.AMAZON; @BeforeEach void setup() { diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java index 4ff6d55..d63d89c 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java @@ -18,7 +18,7 @@ class AmazonSellerTest { private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static Webshop demoDataTarget = Webshop.Amazon; + private static Webshop demoDataTarget = Webshop.AMAZON; @BeforeEach void setup() { diff --git a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java index 5196c8b..c152e5f 100644 --- a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java @@ -1,8 +1,6 @@ package de.rwu.easydrop.api.client; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; import javax.naming.ConfigurationException; @@ -11,10 +9,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.sqlite.SQLiteDataSource; -import de.rwu.easydrop.data.connector.SQLiteConnector; -import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.util.Config; class DataSourceFactoryTest { @@ -50,20 +45,4 @@ class DataSourceFactoryTest { // Assert assertEquals("ebay-api-key", DataSource.getApiKey()); } - - @Test - void createProductPersistenceDataSource_NullPersistence() { - PersistenceException exception = assertThrows(PersistenceException.class, () -> { - DataSourceFactory.createProductPersistenceDataSource(); - }); - - assertEquals("Persistence is not set", exception.getMessage()); - } - - @Test - void createProductPersistenceDataSource_WorkingPersistence() { - DataSourceFactory.setPersistence(new SQLiteConnector(new SQLiteDataSource())); - - assertDoesNotThrow(() -> DataSourceFactory.createProductPersistenceDataSource()); - } } diff --git a/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java index a311c73..1615b44 100644 --- a/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java @@ -26,7 +26,7 @@ class EbayItemDataSourceTest { private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static Webshop demoDataOrigin = Webshop.eBay; + private static Webshop demoDataOrigin = Webshop.EBAY; private static String demoQuery = "iPhone"; @BeforeEach @@ -83,7 +83,7 @@ class EbayItemDataSourceTest { ProductDTO result = demoDataSource.buildProductDTO(product, json); - assertEquals(Webshop.eBay, result.getDataOrigin()); + assertEquals(Webshop.EBAY, result.getDataOrigin()); assertEquals(false, result.isAvailable()); // Default value for boolean assertEquals(0.0, result.getCurrentPrice()); // Default value for double assertEquals(0.0, result.getDeliveryPrice()); // Default value for double @@ -121,6 +121,6 @@ class EbayItemDataSourceTest { }); // Verify the exception message - assertEquals("Nothing found: eBay API responded with error code 404", exception.getMessage()); + assertEquals("Nothing found: EBAY API responded with error code 404", exception.getMessage()); } } diff --git a/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java index 2b2104c..d303e0b 100644 --- a/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java @@ -18,7 +18,7 @@ class EbayPurchaserTest { private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static Webshop demoDataTarget = Webshop.eBay; + private static Webshop demoDataTarget = Webshop.EBAY; @BeforeEach void setup() { diff --git a/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java b/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java index 0fe0df0..e9f38ac 100644 --- a/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java @@ -18,7 +18,7 @@ class EbaySellerTest { private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static Webshop demoDataTarget = Webshop.eBay; + private static Webshop demoDataTarget = Webshop.EBAY; @BeforeEach void setup() { diff --git a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java index 647babb..3c19d29 100644 --- a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java +++ b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java @@ -14,7 +14,7 @@ class ProductDTOTest { void constructor_SetsProductIdAndDataOrigin() { // Arrange String productId = "12345"; - Webshop dataOrigin = Webshop.Amazon; + Webshop dataOrigin = Webshop.AMAZON; // Act ProductDTO productDTO = new ProductDTO(productId, dataOrigin); @@ -27,19 +27,19 @@ class ProductDTOTest { @Test void gettersAndSetters_WorkAsExpected() { // Arrange - ProductDTO productDTO = new ProductDTO("12345", Webshop.Amazon); + ProductDTO productDTO = new ProductDTO("12345", Webshop.AMAZON); // Act and Assert assertEquals("12345", productDTO.getProductId()); - assertEquals(Webshop.Amazon, productDTO.getDataOrigin()); + assertEquals(Webshop.AMAZON, productDTO.getDataOrigin()); // Modify fields productDTO.setProductId("54321"); - productDTO.setDataOrigin(Webshop.eBay); + productDTO.setDataOrigin(Webshop.EBAY); // Assert assertEquals("54321", productDTO.getProductId()); - assertEquals(Webshop.eBay, productDTO.getDataOrigin()); + assertEquals(Webshop.EBAY, productDTO.getDataOrigin()); } @Test diff --git a/src/test/java/de/rwu/easydrop/core/OfferIdentifierTest.java b/src/test/java/de/rwu/easydrop/core/OfferIdentifierTest.java deleted file mode 100644 index 92d7876..0000000 --- a/src/test/java/de/rwu/easydrop/core/OfferIdentifierTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.core; - -import org.junit.jupiter.api.Test; - -public class OfferIdentifierTest { - @Test - void testRunIdentifier() { - - } -} diff --git a/src/test/java/de/rwu/easydrop/core/OfferProvisionerTest.java b/src/test/java/de/rwu/easydrop/core/OfferProvisionerTest.java deleted file mode 100644 index 6b3b26a..0000000 --- a/src/test/java/de/rwu/easydrop/core/OfferProvisionerTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.core; - -import org.junit.jupiter.api.Test; - -public class OfferProvisionerTest { - @Test - void testRunProvisioner() { - - } -} diff --git a/src/test/java/de/rwu/easydrop/core/OfferReviewerTest.java b/src/test/java/de/rwu/easydrop/core/OfferReviewerTest.java deleted file mode 100644 index 9c03f5c..0000000 --- a/src/test/java/de/rwu/easydrop/core/OfferReviewerTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.core; - -import org.junit.jupiter.api.Test; - -public class OfferReviewerTest { - @Test - void testRunReviewer() { - - } -} diff --git a/src/test/java/de/rwu/easydrop/core/OfferUpdaterTest.java b/src/test/java/de/rwu/easydrop/core/OfferUpdaterTest.java deleted file mode 100644 index b7e46ae..0000000 --- a/src/test/java/de/rwu/easydrop/core/OfferUpdaterTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.core; - -import org.junit.jupiter.api.Test; - -public class OfferUpdaterTest { - @Test - void testRunUpdater() { - - } -} diff --git a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java index 9c513db..3dcfd84 100644 --- a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java +++ b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java @@ -11,11 +11,8 @@ import static org.mockito.Mockito.doThrow; import java.sql.SQLException; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.sqlite.SQLiteDataSource; @@ -24,30 +21,24 @@ import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.model.Webshop; -@TestInstance(Lifecycle.PER_CLASS) class SQLiteConnectorTest { private static final String TEST_PRODUCT_ID = "12345"; - private SQLiteConnector sqliteConnector; + private SQLiteConnector sqliteConnector = new SQLiteConnector(new SQLiteDataSource()); @Mock private SQLiteDataSource mockDataSource; - @BeforeAll - public void setup() { - sqliteConnector = new SQLiteConnector(new SQLiteDataSource()); - } - @BeforeEach - public void clearDatabase() { + public void prepare() { MockitoAnnotations.openMocks(this); } @Test - void saveProduct_ValidProduct_SuccessfullySaved() { + void writeProduct_ValidProduct_SuccessfullySaved() { // Arrange sqliteConnector.clearData(); - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.Amazon); - ProductDTO.setDataOrigin(Webshop.Amazon); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON); + ProductDTO.setDataOrigin(Webshop.AMAZON); ProductDTO.setProductId(TEST_PRODUCT_ID); ProductDTO.setCurrentPrice(9.99); ProductDTO.setMerchant("Sample Merchant"); @@ -55,12 +46,12 @@ class SQLiteConnectorTest { ProductDTO.setAvailable(true); // Act - assertDoesNotThrow(() -> sqliteConnector.saveProduct(ProductDTO)); + assertDoesNotThrow(() -> sqliteConnector.writeProduct(ProductDTO)); // Assert ProductDTO savedProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID); assertNotNull(savedProductDTO); - assertEquals(Webshop.Amazon, savedProductDTO.getDataOrigin()); + assertEquals(Webshop.AMAZON, savedProductDTO.getDataOrigin()); assertEquals(TEST_PRODUCT_ID, savedProductDTO.getProductId()); assertEquals(9.99, savedProductDTO.getCurrentPrice()); assertEquals("Sample Merchant", savedProductDTO.getMerchant()); @@ -79,7 +70,7 @@ class SQLiteConnectorTest { // Assert assertNotNull(ProductDTO); - assertEquals(Webshop.Amazon, ProductDTO.getDataOrigin()); + assertEquals(Webshop.AMAZON, ProductDTO.getDataOrigin()); assertEquals(TEST_PRODUCT_ID, ProductDTO.getProductId()); assertEquals(9.99, ProductDTO.getCurrentPrice()); assertEquals("Sample Merchant", ProductDTO.getMerchant()); @@ -112,14 +103,14 @@ class SQLiteConnectorTest { } @Test - void saveProduct_ThrowsPersistenceException_OnSQLException() throws SQLException { + void writeProduct_ThrowsPersistenceException_OnSQLException() throws SQLException { // Arrange - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.Amazon); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON); sqliteConnector.setDb(mockDataSource); doThrow(SQLException.class).when(mockDataSource).getConnection(); // Act and Assert - assertThrows(PersistenceException.class, () -> sqliteConnector.saveProduct(ProductDTO)); + assertThrows(PersistenceException.class, () -> sqliteConnector.writeProduct(ProductDTO)); } @Test @@ -144,41 +135,11 @@ class SQLiteConnectorTest { } private void insertSampleProduct() { - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.Amazon); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON); ProductDTO.setCurrentPrice(9.99); ProductDTO.setMerchant("Sample Merchant"); ProductDTO.setDeliveryPrice(2.50); ProductDTO.setAvailable(true); - sqliteConnector.saveProduct(ProductDTO); - } - - @Test - void getApiKey_UnsupportedOperationExceptionThrown() { - // Arrange - SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); - - // Act and Assert - assertThrows(UnsupportedOperationException.class, connector::getApiKey); - } - - @Test - void buildProductDTO_UnsupportedOperationExceptionThrown() { - // Arrange - SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); - ProductDTO product = new ProductDTO("ASIN123", Webshop.Amazon); - String json = "{\"productId\":\"ASIN123\",\"dataOrigin\":\"Amazon\"}"; - - // Act and Assert - assertThrows(UnsupportedOperationException.class, () -> connector.buildProductDTO(product, json)); - } - - @Test - void createApiUrl_UnsupportedOperationExceptionThrown() { - // Arrange - SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); - String productIdentifier = "ASIN123"; - - // Act and Assert - assertThrows(UnsupportedOperationException.class, () -> connector.createApiUrl(productIdentifier)); + sqliteConnector.writeProduct(ProductDTO); } } diff --git a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java index c83f84f..ebdcab4 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java @@ -1,5 +1,7 @@ package de.rwu.easydrop.model; +import static org.mockito.Mockito.description; + import java.util.List; import org.junit.jupiter.api.Assertions; @@ -19,7 +21,7 @@ class ProductCatalogueTest { Product product = new Product(); product.setProductId("12345"); product.setMerchant("AmazonSeller"); - product.setDataOrigin(Webshop.Amazon); + product.setDataOrigin(Webshop.AMAZON); productCatalogue.addProduct(product); List products = productCatalogue.getProducts(); @@ -32,13 +34,13 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin(Webshop.Amazon); + product1.setDataOrigin(Webshop.AMAZON); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); product2.setMerchant("eBaySeller"); - product2.setDataOrigin(Webshop.eBay); + product2.setDataOrigin(Webshop.EBAY); productCatalogue.addProduct(product2); productCatalogue.removeProduct(product1); @@ -53,13 +55,13 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin(Webshop.Amazon); + product1.setDataOrigin(Webshop.AMAZON); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); product2.setMerchant("ebayMerchant"); - product2.setDataOrigin(Webshop.eBay); + product2.setDataOrigin(Webshop.EBAY); productCatalogue.addProduct(product2); productCatalogue.clearProducts(); @@ -73,20 +75,23 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin(Webshop.Amazon); + product1.setDataOrigin(Webshop.AMAZON); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); product2.setMerchant("eBaySeller"); - product2.setDataOrigin(Webshop.eBay); + product2.setDataOrigin(Webshop.EBAY); productCatalogue.addProduct(product2); - String expectedString = "Product Catalogue: GPU\n" + - "Description: Graphics Processing Units\n" + - "Products:\n" + - "Product: [12345 from AmazonSeller (Amazon) at 0,00 Euro (available: no)]\n" + - "Product: [54321 from eBaySeller (eBay) at 0,00 Euro (available: no)]\n"; + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Catalogue Name: %s%n", "GPU")); + sb.append(String.format("Description: %s%n", "Graphics Processing Units")); + sb.append("Products:\n"); + sb.append(String.format("%s%n", product1.toString())); + sb.append(String.format("%s%n", product2.toString())); + + String expectedString = sb.toString(); Assertions.assertEquals(expectedString, productCatalogue.toString()); } diff --git a/src/test/java/de/rwu/easydrop/model/ProductTest.java b/src/test/java/de/rwu/easydrop/model/ProductTest.java index 2c5c1c2..10c8278 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductTest.java @@ -10,13 +10,13 @@ class ProductTest { @Test void testToString1() { Product product1 = new Product(); - product1.setDataOrigin(Webshop.Amazon); + product1.setDataOrigin(Webshop.AMAZON); product1.setProductId("12345"); product1.setMerchant("Merchant A"); product1.setCurrentPrice(19.99); product1.setAvailable(true); - String expectedString1 = "Product: [12345 from Merchant A (Amazon) at 19,99 Euro (available: yes)]"; + String expectedString1 = "Product: [12345 from Merchant A (AMAZON) at 19,99 Euro (available: yes)]"; String result1 = product1.toString(); assertEquals(expectedString1, result1); @@ -25,13 +25,13 @@ class ProductTest { @Test void testToString2() { Product product2 = new Product(); - product2.setDataOrigin(Webshop.eBay); + product2.setDataOrigin(Webshop.EBAY); product2.setProductId("67890"); product2.setMerchant("Merchant B"); product2.setCurrentPrice(9.99); product2.setAvailable(false); - String expectedString2 = "Product: [67890 from Merchant B (eBay) at 9,99 Euro (available: no)]"; + String expectedString2 = "Product: [67890 from Merchant B (EBAY) at 9,99 Euro (available: no)]"; String result2 = product2.toString(); assertEquals(expectedString2, result2); @@ -41,7 +41,7 @@ class ProductTest { void gettersAndSetters_WorkAsExpected() { // Arrange Product product = new Product(); - product.setDataOrigin(Webshop.Amazon); + product.setDataOrigin(Webshop.AMAZON); product.setProductId("12345"); product.setCurrentPrice(9.99); product.setMerchant("Example Merchant"); @@ -49,7 +49,7 @@ class ProductTest { product.setAvailable(true); // Act and Assert - assertEquals(Webshop.Amazon, product.getDataOrigin()); + assertEquals(Webshop.AMAZON, product.getDataOrigin()); assertEquals("12345", product.getProductId()); assertEquals(9.99, product.getCurrentPrice()); assertEquals("Example Merchant", product.getMerchant()); @@ -57,7 +57,7 @@ class ProductTest { assertTrue(product.isAvailable()); // Modify fields - product.setDataOrigin(Webshop.eBay); + product.setDataOrigin(Webshop.EBAY); product.setProductId("54321"); product.setCurrentPrice(19.99); product.setMerchant("New Merchant"); @@ -65,7 +65,7 @@ class ProductTest { product.setAvailable(false); // Assert - assertEquals(Webshop.eBay, product.getDataOrigin()); + assertEquals(Webshop.EBAY, product.getDataOrigin()); assertEquals("54321", product.getProductId()); assertEquals(19.99, product.getCurrentPrice()); assertEquals("New Merchant", product.getMerchant()); diff --git a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java index 9212a24..70bae6e 100644 --- a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java +++ b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java @@ -48,7 +48,7 @@ class ProductMapperTest { } private ProductDTO createProductDTO() { - ProductDTO dto = new ProductDTO("12345", Webshop.Amazon); + ProductDTO dto = new ProductDTO("12345", Webshop.AMAZON); dto.setAvailable(true); dto.setCurrentPrice(9.99); dto.setDeliveryPrice(2.50); @@ -61,7 +61,7 @@ class ProductMapperTest { // Arrange Product product = new Product(); product.setProductId("12345"); - product.setDataOrigin(Webshop.Amazon); + product.setDataOrigin(Webshop.AMAZON); product.setAvailable(true); product.setCurrentPrice(9.99); product.setDeliveryPrice(2.50); @@ -72,7 +72,7 @@ class ProductMapperTest { // Assert assertEquals("12345", dto.getProductId()); - assertEquals(Webshop.Amazon, dto.getDataOrigin()); + assertEquals(Webshop.AMAZON, dto.getDataOrigin()); assertTrue(dto.isAvailable()); assertEquals(9.99, dto.getCurrentPrice()); assertEquals(2.50, dto.getDeliveryPrice()); diff --git a/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java b/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java deleted file mode 100644 index 2ffb76a..0000000 --- a/src/test/java/de/rwu/easydrop/service/processing/OrderManagerTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package de.rwu.easydrop.service.processing; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -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 java.util.ArrayList; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import de.rwu.easydrop.exception.InvalidCatalogueException; -import de.rwu.easydrop.model.Product; -import de.rwu.easydrop.model.ProductCatalogue; -import de.rwu.easydrop.model.ProductPair; - -class OrderManagerTest { - - @Test - void testConstructorIsPrivate() - throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - // Check for private constructor - Constructor constructor = OrderManager.class.getDeclaredConstructor(); - assertTrue(Modifier.isPrivate(constructor.getModifiers())); - - // Make sure exception is thrown when instantiating - constructor.setAccessible(true); - assertThrows(InvocationTargetException.class, () -> { - constructor.newInstance(); - }); - } - - @Test - void testCreateOrders_ValidCatalogues_OrdersCreated() { - // Arrange - List catalogues = createSampleCatalogues(); - - // Act - assertDoesNotThrow(() -> { - OrderManager.createOrders(catalogues); - }); - } - - @Test - void testGetHighestMarginProducts_ValidCatalogue_CheapestAndMostExpensiveProductsReturned() { - // Arrange - ProductCatalogue catalogue = createSampleCatalogue(); - - // Act - ProductPair pair = OrderManager.getHighestMarginProducts(catalogue); - - // Assert - assertNotNull(pair); - assertNotNull(pair.getProduct1()); - assertNotNull(pair.getProduct2()); - assertEquals("Product2", pair.getProduct1().getProductId()); - assertEquals("Product3", pair.getProduct2().getProductId()); - } - - @Test - void testGetHighestMarginProducts_InvalidCatalogue_ThrowsInvalidCatalogueException() { - // Arrange - ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); - catalogue.addProduct(new Product()); - // The catalogue has only one product, which is invalid - - // Act & Assert - Exception e = assertThrows(InvalidCatalogueException.class, - () -> OrderManager.getHighestMarginProducts(catalogue)); - - assertEquals("Product Catalogue holds less than 2 products!", e.getMessage()); - } - - @Test - void testGetHighestMarginProducts_InvalidCatalogue_NoMargin() { - // Arrange - ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); - catalogue.addProduct(new Product()); - catalogue.addProduct(new Product()); - // The catalogue has only one product, which is invalid - - // Act & Assert - Exception e = assertThrows(InvalidCatalogueException.class, - () -> OrderManager.getHighestMarginProducts(catalogue)); - - assertEquals("Price margin is zero!", e.getMessage()); - } - - private List createSampleCatalogues() { - List catalogues = new ArrayList<>(); - - ProductCatalogue catalogue1 = createSampleCatalogue(); - catalogues.add(catalogue1); - - ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue2", "Sample catalogue 2"); - Product product4 = new Product(); - product4.setProductId("Product4"); - product4.setCurrentPrice(6.78); - catalogue2.addProduct(product4); - Product product5 = new Product(); - product5.setProductId("Product5"); - product5.setCurrentPrice(7.89); - catalogue2.addProduct(product5); - catalogues.add(catalogue2); - - return catalogues; - } - - private ProductCatalogue createSampleCatalogue() { - ProductCatalogue catalogue = new ProductCatalogue("Catalogue1", "Sample catalogue"); - Product product1 = new Product(); - product1.setProductId("Product1"); - product1.setCurrentPrice(1.23); - catalogue.addProduct(product1); - Product product2 = new Product(); - product2.setProductId("Product2"); - product2.setCurrentPrice(0.89); - catalogue.addProduct(product2); - Product product3 = new Product(); - product3.setProductId("Product3"); - product3.setCurrentPrice(4.56); - catalogue.addProduct(product3); - return catalogue; - } -} diff --git a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java index edd772a..b13c571 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java @@ -38,20 +38,20 @@ class CatalogueRetrieverTest { // Create a sample product catalogue with two products ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue"); Product product1 = new Product(); - product1.setDataOrigin(Webshop.Amazon); + product1.setDataOrigin(Webshop.AMAZON); product1.setProductId("ASIN1"); productCatalogue.addProduct(product1); Product product2 = new Product(); - product2.setDataOrigin(Webshop.eBay); + product2.setDataOrigin(Webshop.EBAY); product2.setProductId("ProductID2"); productCatalogue.addProduct(product2); productCatalogues.add(productCatalogue); // Mock the methods when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues); - when(productRetriever.getProductFromWebshop(Webshop.Amazon, "ASIN1")).thenReturn(product1); - when(productRetriever.getProductFromWebshop(Webshop.eBay, "ProductID2")).thenReturn(product2); + when(productRetriever.getProductFromWebshop(Webshop.AMAZON, "ASIN1")).thenReturn(product1); + when(productRetriever.getProductFromWebshop(Webshop.EBAY, "ProductID2")).thenReturn(product2); // Act catalogueRetriever.loadCatalogues(); @@ -70,7 +70,7 @@ class CatalogueRetrieverTest { // Verify the method invocations verify(productsConfig).loadConfig(); - verify(productRetriever).getProductFromWebshop(Webshop.Amazon, "ASIN1"); - verify(productRetriever).getProductFromWebshop(Webshop.eBay, "ProductID2"); + verify(productRetriever).getProductFromWebshop(Webshop.AMAZON, "ASIN1"); + verify(productRetriever).getProductFromWebshop(Webshop.EBAY, "ProductID2"); } } diff --git a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java index 5fee30f..da42da6 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java @@ -17,7 +17,7 @@ import de.rwu.easydrop.api.client.AmazonProductDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.ProductDTO; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.util.Config; @@ -26,7 +26,7 @@ class ProductRetrieverTest { @Mock private Config config; @Mock - private DataSourceFactory DataSourceFactory; + private DataSourceFactory dataSourceFactory; @Mock private AmazonProductDataSource amazonDataSource; @Mock @@ -36,7 +36,7 @@ class ProductRetrieverTest { @Mock private Product product; @Mock - private AbstractProductPersistence persistence; + private ProductPersistenceInterface persistence; private ProductRetriever productRetriever; @@ -45,19 +45,19 @@ class ProductRetrieverTest { MockitoAnnotations.openMocks(this); when(config.getProperty("AMAZON_API_URL")).thenReturn("https://api.amazon.com"); when(config.getProperty("AMAZON_API_KEY")).thenReturn("amazon-api-key"); - DataSourceFactory.setConfig(config); - productRetriever = new ProductRetriever(DataSourceFactory); + dataSourceFactory.setConfig(config); + productRetriever = new ProductRetriever(dataSourceFactory, persistence); } @Test void getProductFromAmazon_ReturnsProduct() { // Arrange String asin = "B01234ABC"; - when(DataSourceFactory.createAmazonProductDataSource()).thenReturn(amazonDataSource); + when(dataSourceFactory.createAmazonProductDataSource()).thenReturn(amazonDataSource); when(amazonDataSource.getProductDTOById(asin)).thenReturn(productDTO); when(productDTO.getProductId()).thenReturn(asin); when(productDTO.getCurrentPrice()).thenReturn(9.99); - when(productDTO.getDataOrigin()).thenReturn(Webshop.Amazon); + when(productDTO.getDataOrigin()).thenReturn(Webshop.AMAZON); // Act Product result = productRetriever.getProductFromAmazon(asin); @@ -73,11 +73,11 @@ class ProductRetrieverTest { void getProductFromEbay_ReturnsProduct() { // Arrange String productQuery = "MySearchQuery"; - when(DataSourceFactory.createEbayItemDataSource()).thenReturn(ebayDataSource); + when(dataSourceFactory.createEbayItemDataSource()).thenReturn(ebayDataSource); when(ebayDataSource.getProductDTOById(productQuery)).thenReturn(productDTO); when(productDTO.getProductId()).thenReturn(productQuery); when(productDTO.getCurrentPrice()).thenReturn(9.99); - when(productDTO.getDataOrigin()).thenReturn(Webshop.eBay); + when(productDTO.getDataOrigin()).thenReturn(Webshop.EBAY); // Act Product result = productRetriever.getProductFromEbay(productQuery); @@ -93,11 +93,10 @@ class ProductRetrieverTest { void getProductFromPersistence_ValidProductId_ReturnsProduct() { // Arrange String productId = "123"; - when(DataSourceFactory.createProductPersistenceDataSource()).thenReturn(persistence); when(persistence.getProductDTOById(productId)).thenReturn(productDTO); when(productDTO.getProductId()).thenReturn(productId); when(productDTO.getCurrentPrice()).thenReturn(9.99); - when(productDTO.getDataOrigin()).thenReturn(Webshop.Amazon); + when(productDTO.getDataOrigin()).thenReturn(Webshop.AMAZON); // Act Product result = productRetriever.getProductFromPersistence(productId); diff --git a/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java index 1ad2f6b..4058ea7 100644 --- a/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java +++ b/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java @@ -38,7 +38,7 @@ class ProductValidatorTest { // Arrange Product product = new Product(); product.setCurrentPrice(9.99); - product.setDataOrigin(Webshop.Amazon); + product.setDataOrigin(Webshop.AMAZON); product.setProductId("12345"); // Act and Assert @@ -61,7 +61,7 @@ class ProductValidatorTest { private static Product createProductWithZeroPrice() { Product product = new Product(); product.setCurrentPrice(0.00); - product.setDataOrigin(Webshop.Amazon); + product.setDataOrigin(Webshop.AMAZON); product.setProductId("12345"); return product; } @@ -69,7 +69,7 @@ class ProductValidatorTest { private static Product createProductWithEmptyProductId() { Product product = new Product(); product.setCurrentPrice(9.99); - product.setDataOrigin(Webshop.Amazon); + product.setDataOrigin(Webshop.AMAZON); product.setProductId(""); return product; } diff --git a/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java index c590733..22eb73b 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java @@ -13,7 +13,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.model.Webshop; @@ -21,7 +21,7 @@ import de.rwu.easydrop.model.Webshop; class CatalogueWriterTest { @Mock - private AbstractProductPersistence persistenceMock; + private ProductPersistenceInterface persistenceMock; private CatalogueWriter catalogueWriter; @@ -40,19 +40,19 @@ class CatalogueWriterTest { catalogueWriter.writeCatalogues(catalogues); // Assert - verify(persistenceMock, times(4)).saveProduct(any(ProductDTO.class)); + verify(persistenceMock, times(4)).writeProduct(any(ProductDTO.class)); } private List createSampleCatalogues() { List catalogues = new ArrayList<>(); ProductCatalogue catalogue1 = new ProductCatalogue("Catalogue 1", "Sample catalogue 1"); - catalogue1.addProduct(createSampleProduct(Webshop.Amazon, "ID 1")); - catalogue1.addProduct(createSampleProduct(Webshop.eBay, "ID 2")); + catalogue1.addProduct(createSampleProduct(Webshop.AMAZON, "ID 1")); + catalogue1.addProduct(createSampleProduct(Webshop.EBAY, "ID 2")); ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue 2", "Sample catalogue 2"); - catalogue2.addProduct(createSampleProduct(Webshop.Amazon, "ID 3")); - catalogue2.addProduct(createSampleProduct(Webshop.eBay, "ID 4")); + catalogue2.addProduct(createSampleProduct(Webshop.AMAZON, "ID 3")); + catalogue2.addProduct(createSampleProduct(Webshop.EBAY, "ID 4")); catalogues.add(catalogue1); catalogues.add(catalogue2); diff --git a/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java index e2bd870..60f9198 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java @@ -10,22 +10,21 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.Webshop; class ProductWriterTest { @Mock - private AbstractProductPersistence persistence; + private ProductPersistenceInterface persistence; private ProductWriter productWriter; @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); - productWriter = new ProductWriter(); - productWriter.setPersistence(persistence); + productWriter = new ProductWriter(persistence); } @Test @@ -33,14 +32,14 @@ class ProductWriterTest { // Arrange Product product = new Product(); product.setProductId("12345"); - product.setDataOrigin(Webshop.Amazon); + product.setDataOrigin(Webshop.AMAZON); product.setCurrentPrice(9.99); // Act productWriter.writeProductToPersistence(product); // Assert - Mockito.verify(persistence).saveProduct(any(ProductDTO.class)); + Mockito.verify(persistence).writeProduct(any(ProductDTO.class)); } @Test @@ -48,7 +47,7 @@ class ProductWriterTest { // Arrange Product product = new Product(); product.setProductId(""); - product.setDataOrigin(Webshop.Amazon); + product.setDataOrigin(Webshop.AMAZON); // Act and Assert assertThrows(Exception.class, () -> productWriter.writeProductToPersistence(product)); diff --git a/src/test/resources/test.malformed.products-config.json b/src/test/resources/test.malformed.products-config.json index 00d0092..1d7c6fe 100644 --- a/src/test/resources/test.malformed.products-config.json +++ b/src/test/resources/test.malformed.products-config.json @@ -5,10 +5,10 @@ description: "Integration Testing Product", "identifiers": [ { - "Amazon": "DEMO-AMAZON-001" + "AMAZON": "DEMO-AMAZON-001" }, { - "eBay": "DEMO-EBAY-001" + "EBAY": "DEMO-EBAY-001" } ] ] diff --git a/src/test/resources/test.products-config.json b/src/test/resources/test.products-config.json index 44262c8..c48cfc6 100644 --- a/src/test/resources/test.products-config.json +++ b/src/test/resources/test.products-config.json @@ -5,10 +5,10 @@ "description": "Integration Testing Product", "identifiers": [ { - "Amazon": "DEMO-AMAZON-001" + "AMAZON": "DEMO-AMAZON-001" }, { - "eBay": "DEMO-EBAY-001" + "EBAY": "DEMO-EBAY-001" } ] } From d80f5cf00c0fed366629546c526a2091211721dc Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 15:56:21 +0200 Subject: [PATCH 44/71] created test class "OfferRetrieverTest" --- .../service/retriever/OfferRetrieverTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java diff --git a/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java new file mode 100644 index 0000000..f29059a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java @@ -0,0 +1,85 @@ +package de.rwu.easydrop.service.retriever; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.client.AmazonProductDataSource; +import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.api.client.EbayItemDataSource; +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.util.Config; + + + + +class OfferRetrieverTest { + + @Mock + private Config config; + @Mock + private DataSourceFactory dataSourceFactory; + @Mock + private AmazonProductDataSource amazonDataSource; + @Mock + private EbayItemDataSource ebayDataSource; + @Mock + private OfferDTO offerDTO; + @Mock + private Offer offer; + @Mock + private OfferPersistenceInterface persistence; + + private OfferRetriever offerRetriever; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + when(config.getProperty("AMAZON_API_URL")).thenReturn("https://api.amazon.com"); + when(config.getProperty("AMAZON_API_KEY")).thenReturn("amazon-api-key"); + dataSourceFactory.setConfig(config); + offerRetriever = new OfferRetriever(persistence); + } + + @Test + void getOfferFromPersistence(){ + //Arrange + String offerId = "187"; + String lastUpdate = "2023-01-01"; + when(persistence.getOfferDTOById(offerId)).thenReturn(offerDTO); + when(offerDTO.getOfferId()).thenReturn(offerId); + when(offerDTO.getLastUpdate()).thenReturn(lastUpdate); + + //Act + Offer result = offerRetriever.getOfferFromPersistence(offerId); + + //Assert + assertEquals(offerId, result.getOfferId()); + assertEquals(lastUpdate, result.getLastUpdate()); + + //Verify + verify(persistence, times(1)).getOfferDTOById(offerId); + + } + + + + + + + + + + + +} From e0f30e4710e0b1b139f5e21a86110a76e7f77d25 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 15:58:10 +0200 Subject: [PATCH 45/71] created test getOfferFromPersistence --- src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java index ebdcab4..df78e3e 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java @@ -1,6 +1,6 @@ package de.rwu.easydrop.model; -import static org.mockito.Mockito.description; + import java.util.List; From 09cfb6adfab4983d8e6e5ac1f9df99b67407b6b6 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 16:14:55 +0200 Subject: [PATCH 46/71] created product mock object --- .../easydrop/service/retriever/OfferRetrieverTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java index f29059a..d994e46 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java @@ -1,6 +1,7 @@ package de.rwu.easydrop.service.retriever; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -16,6 +17,7 @@ import de.rwu.easydrop.api.client.AmazonProductDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.data.connector.OfferPersistenceInterface; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.util.Config; @@ -53,6 +55,12 @@ class OfferRetrieverTest { @Test void getOfferFromPersistence(){ + + // a mock ProductDTO object to return from the offerDTO mock + ProductDTO productDTO = mock(ProductDTO.class); + when(productDTO.isAvailable()).thenReturn(false); + + //Arrange String offerId = "187"; String lastUpdate = "2023-01-01"; From b1233922bdcf10fd74680303f68a065e8447d479 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 16:20:25 +0200 Subject: [PATCH 47/71] offerdto instance erstellt --- src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java index 8ffd6a8..479c052 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java @@ -28,4 +28,14 @@ public class OfferDTO { * Date of last update of the offer. */ private String lastUpdate; + + /** + * Creates OfferDTO instance. + * @param newofferId + * @param newLastUpdate + */ + public OfferDTO(final String newofferId, final String newLastUpdate){ + this.offerId = newofferId; + this.lastUpdate = newLastUpdate; + } } From 6ca2027123527eeb2bcec4c84e666613df0ef48c Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 16:41:25 +0200 Subject: [PATCH 48/71] fixed test "getOfferFromPersistence" --- .../service/retriever/OfferRetrieverTest.java | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java index d994e46..aa95f42 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java @@ -23,8 +23,6 @@ import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.util.Config; - - class OfferRetrieverTest { @Mock @@ -56,10 +54,9 @@ class OfferRetrieverTest { @Test void getOfferFromPersistence(){ - // a mock ProductDTO object to return from the offerDTO mock + //Mock ProductDTO object to return from the offerDTO mock ProductDTO productDTO = mock(ProductDTO.class); - when(productDTO.isAvailable()).thenReturn(false); - + when(productDTO.isAvailable()).thenReturn(true); //Arrange String offerId = "187"; @@ -67,6 +64,8 @@ class OfferRetrieverTest { when(persistence.getOfferDTOById(offerId)).thenReturn(offerDTO); when(offerDTO.getOfferId()).thenReturn(offerId); when(offerDTO.getLastUpdate()).thenReturn(lastUpdate); + when(offerDTO.getSourceProduct()).thenReturn(productDTO); + when(offerDTO.getTargetProduct()).thenReturn(productDTO); //Act Offer result = offerRetriever.getOfferFromPersistence(offerId); @@ -80,14 +79,4 @@ class OfferRetrieverTest { } - - - - - - - - - - } From 19c796b457f2df7c2586674232ada5e5ba7aa36b Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 27 Jun 2023 16:41:55 +0200 Subject: [PATCH 49/71] #75 Added transaction base classes --- .../rwu/easydrop/api/dto/TransactionDTO.java | 28 ++++++++++ .../de/rwu/easydrop/model/Transaction.java | 30 +++++++++++ .../service/mapping/TransactionMapper.java | 54 +++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 src/main/java/de/rwu/easydrop/api/dto/TransactionDTO.java create mode 100644 src/main/java/de/rwu/easydrop/model/Transaction.java create mode 100644 src/main/java/de/rwu/easydrop/service/mapping/TransactionMapper.java diff --git a/src/main/java/de/rwu/easydrop/api/dto/TransactionDTO.java b/src/main/java/de/rwu/easydrop/api/dto/TransactionDTO.java new file mode 100644 index 0000000..630b2ce --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/dto/TransactionDTO.java @@ -0,0 +1,28 @@ +package de.rwu.easydrop.api.dto; + +import lombok.Data; + +/** + * Transaction data transfer object. + * + * @since 0.3.0 + */ +@Data +public class TransactionDTO { + /** + * Offer ID. + */ + private String offerId; + /** + * Sales volume. + */ + private double volume; + /** + * Earnings (volume - cost). + */ + private double earnings; + /** + * Transaction timestamp. + */ + private String transactionTime; +} diff --git a/src/main/java/de/rwu/easydrop/model/Transaction.java b/src/main/java/de/rwu/easydrop/model/Transaction.java new file mode 100644 index 0000000..a8236fb --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/Transaction.java @@ -0,0 +1,30 @@ +package de.rwu.easydrop.model; + +import lombok.Data; + +/** + * A transaction. + * + * @since 0.3.0 + */ +@Data +public class Transaction { + /** + * Offer ID. + * + * @see Offer + */ + private String offerId; + /** + * Sales volume. + */ + private double volume; + /** + * Earnings (volume - cost). + */ + private double earnings; + /** + * Transaction timestamp. + */ + private String transactionTime; +} diff --git a/src/main/java/de/rwu/easydrop/service/mapping/TransactionMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/TransactionMapper.java new file mode 100644 index 0000000..47c68f5 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/mapping/TransactionMapper.java @@ -0,0 +1,54 @@ +package de.rwu.easydrop.service.mapping; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.model.Transaction; + +/** + * Maps transaction DTOs and objects. + * + * @since 0.3.0 + */ +public final class TransactionMapper { + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private TransactionMapper() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a mapping class, don't instantiate it."); + } + + /** + * Creates a Transaction object from a corresponding DTO. + * + * @param dto Transaction Data Transfer Object + * @return Product + */ + public static Transaction mapTXFromDTO(final TransactionDTO dto) { + Transaction tx = new Transaction(); + + tx.setOfferId(dto.getOfferId()); + tx.setVolume(dto.getVolume()); + tx.setEarnings(dto.getEarnings()); + tx.setTransactionTime(dto.getTransactionTime()); + + return tx; + } + + /** + * Creates a ProductDTO object from a corresponding Product. + * + * @param tx Transaction + * @return TransactionDTO + */ + public static TransactionDTO mapTXToDTO(final Transaction tx) { + TransactionDTO dto = new TransactionDTO(); + + dto.setOfferId(tx.getOfferId()); + dto.setVolume(tx.getVolume()); + dto.setEarnings(tx.getEarnings()); + dto.setTransactionTime(tx.getTransactionTime()); + + return dto; + } +} From b551f03d160dffc21e61858686d2f86f090d2529 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 16:42:49 +0200 Subject: [PATCH 50/71] fixed checkstyle problems --- src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java index 479c052..8dc04d0 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java @@ -29,13 +29,5 @@ public class OfferDTO { */ private String lastUpdate; - /** - * Creates OfferDTO instance. - * @param newofferId - * @param newLastUpdate - */ - public OfferDTO(final String newofferId, final String newLastUpdate){ - this.offerId = newofferId; - this.lastUpdate = newLastUpdate; - } + } From f9ea1f65ca3a7d68803d074385e6367d85f76a13 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 17:19:32 +0200 Subject: [PATCH 51/71] created test test for writeOffer --- .../service/writer/OfferWriterTest.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java diff --git a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java new file mode 100644 index 0000000..2d966bd --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java @@ -0,0 +1,62 @@ +package de.rwu.easydrop.service.writer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import javax.sound.sampled.AudioFileFormat.Type; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; + +public class OfferWriterTest { + + @Mock + private OfferDTO offerDTO; + @Mock + private OfferPersistenceInterface persistence; + + private OfferWriter offerWriter; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + offerWriter = new OfferWriter(persistence); + } + + + + + @Test + void writeOfferToPersistence_InvalidProduct_ThrowsException() { + //Arrange + Offer offer = new Offer(); + offer.setOfferId(""); + offer.setLastUpdate(""); + + //Act and Assert + assertThrows(Exception.class, () -> offerWriter.writeOfferToPersistence(offer)); + } + + + + + + + + + + +} From 23c8a8c95a94ce71cce2b753e625478487357591 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 17:23:05 +0200 Subject: [PATCH 52/71] created test write offer with validproduct --- .../service/writer/OfferWriterTest.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java index 2d966bd..ef2d367 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java @@ -36,9 +36,6 @@ public class OfferWriterTest { offerWriter = new OfferWriter(persistence); } - - - @Test void writeOfferToPersistence_InvalidProduct_ThrowsException() { //Arrange @@ -50,6 +47,30 @@ public class OfferWriterTest { assertThrows(Exception.class, () -> offerWriter.writeOfferToPersistence(offer)); } + @Test + void writeOfferToPresistence_ValidProduct_CallsSaveProduct(){ + + // Arrange + Product product = new Product(); + product.setProductId("12345"); + product.setDataOrigin(Webshop.AMAZON); + product.setCurrentPrice(9.99); + + // Arrange + Offer offer = new Offer(); + offer.setOfferId("26876"); + offer.setLastUpdate("2022-12-25"); + offer.setSourceProduct(product); + offer.setTargetProduct(product); + + // Act + offerWriter.writeOfferToPersistence(offer); + + // Assert + Mockito.verify(persistence).writeOffer(any(OfferDTO.class)); + + } + From 1b30e29d145b90fdeacc9ad813166816efb4c0c7 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 17:23:36 +0200 Subject: [PATCH 53/71] created test for writeOffer with valid product --- .../java/de/rwu/easydrop/service/writer/OfferWriterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java index ef2d367..1cbe974 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java @@ -47,7 +47,7 @@ public class OfferWriterTest { assertThrows(Exception.class, () -> offerWriter.writeOfferToPersistence(offer)); } - @Test + @Test void writeOfferToPresistence_ValidProduct_CallsSaveProduct(){ // Arrange From af000158fd19c60ca0de67487cca56cf39ac4d3e Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 17:24:32 +0200 Subject: [PATCH 54/71] deleted unnecessary imports --- .../de/rwu/easydrop/service/writer/OfferWriterTest.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java index 1cbe974..70bf52a 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java @@ -1,12 +1,8 @@ package de.rwu.easydrop.service.writer; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.junit.jupiter.api.Assertions.assertThrows; -import javax.sound.sampled.AudioFileFormat.Type; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,7 +11,6 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.OfferDTO; -import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.data.connector.OfferPersistenceInterface; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.Product; From 41acb18dcbd0d5d760a5a471b95c2ef090063d6f Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 17:26:14 +0200 Subject: [PATCH 55/71] new class for offer validator test --- .../rwu/easydrop/service/validation/OfferValidatorTest.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java diff --git a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java new file mode 100644 index 0000000..f2d4a4b --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java @@ -0,0 +1,5 @@ +package de.rwu.easydrop.service.validation; + +public class OfferValidatorTest { + +} From 0b403d73eab80e6952c9f0edd0bc13ca6eb46588 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 17:54:37 +0200 Subject: [PATCH 56/71] class OfferValidatorTest and test validate created --- .../service/validation/OfferValidatorTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java index f2d4a4b..e7d5b39 100644 --- a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java +++ b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java @@ -1,5 +1,23 @@ package de.rwu.easydrop.service.validation; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.model.Offer; + + public class OfferValidatorTest { + + @Test + void validate_Offer_ValidProduct_NoExceptionThrown() { + // Arrange + Offer offer = new Offer(); + offer.setOfferId("3672"); + offer.setLastUpdate("2021-02-03"); + + // Act and Assert + assertDoesNotThrow(() -> OfferValidator.validate(offer)); + } } From 0eb0f903c6fbd8e7896687e208265b3f755dea34 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 18:10:25 +0200 Subject: [PATCH 57/71] added invalid offerexception test --- .../service/validation/OfferValidatorTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java index e7d5b39..f83c24e 100644 --- a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java +++ b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java @@ -1,9 +1,13 @@ package de.rwu.easydrop.service.validation; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import de.rwu.easydrop.exception.InvalidOfferException; import de.rwu.easydrop.model.Offer; @@ -19,5 +23,13 @@ public class OfferValidatorTest { // Act and Assert assertDoesNotThrow(() -> OfferValidator.validate(offer)); } + + @ParameterizedTest + @MethodSource("invalidOffer") + void validate_InvalidOffer_ThrowsInvalidOfferException(Offer offer) { + // Act and Assert + assertThrows(InvalidOfferException.class, () -> OfferValidator.validate(offer)); + } + } From 7056d34688fde4b2fe4baad758d12786949084b4 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 18:32:15 +0200 Subject: [PATCH 58/71] added ParameterizedTest for offer validator --- .../service/validation/OfferValidator.java | 3 +++ .../validation/OfferValidatorTest.java | 24 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java index 82fbd89..edf5491 100644 --- a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java +++ b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java @@ -28,6 +28,9 @@ public final class OfferValidator { if (offer.getOfferId().equals("")) { throw new InvalidOfferException("Offer ID cannot be empty"); } + if (offer.getLastUpdate().equals("")) { + throw new InvalidOfferException("LastUpdate cannot be empty"); + } } catch (NullPointerException e) { throw new InvalidOfferException("Required information is missing in the offer", e); } diff --git a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java index f83c24e..bb16c4b 100644 --- a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java +++ b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java @@ -3,6 +3,8 @@ package de.rwu.easydrop.service.validation; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -25,11 +27,31 @@ public class OfferValidatorTest { } @ParameterizedTest - @MethodSource("invalidOffer") + @MethodSource("invalidOfferProvider") void validate_InvalidOffer_ThrowsInvalidOfferException(Offer offer) { // Act and Assert assertThrows(InvalidOfferException.class, () -> OfferValidator.validate(offer)); + } + static Stream invalidOfferProvider() { + return Stream.of( + createOfferWithEmptylastUpdate(), + createOfferWithEmptyId()); + } + + private static Offer createOfferWithEmptylastUpdate() { + Offer offer = new Offer(); + offer.setOfferId("3729798"); + offer.setLastUpdate(""); + return offer; + } + + private static Offer createOfferWithEmptyId() { + Offer offer = new Offer(); + offer.setOfferId(""); + offer.setLastUpdate("8798476"); + return offer; + } } From 0151ceb00c3e21d4205c35eb0879b73c2eb748ce Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 18:35:58 +0200 Subject: [PATCH 59/71] offermapper test added --- .../de/rwu/easydrop/service/mapping/OfferMapperTest.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java diff --git a/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java new file mode 100644 index 0000000..02bfb7f --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java @@ -0,0 +1,5 @@ +package de.rwu.easydrop.service.mapping; + +public class OfferMapperTest { + +} From d05ea47e726137a1ed1a5ebbd87015808d4ab4c7 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 19:44:30 +0200 Subject: [PATCH 60/71] created mapOffer test --- .../service/mapping/OfferMapperTest.java | 60 ++++++++++++++++++- .../validation/OfferValidatorTest.java | 2 +- .../service/writer/OfferWriterTest.java | 2 +- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java index 02bfb7f..099d63c 100644 --- a/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java +++ b/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java @@ -1,5 +1,63 @@ package de.rwu.easydrop.service.mapping; -public class OfferMapperTest { +import static org.junit.jupiter.api.Assertions.assertEquals; +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.Test; + +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; + +class OfferMapperTest { + + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = OfferMapper.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + + @Test + void mapOfferToDTO() { + // Arrange + Product product = new Product(); + product.setProductId("12345"); + product.setDataOrigin(Webshop.AMAZON); + product.setAvailable(true); + product.setCurrentPrice(9.99); + product.setDeliveryPrice(2.50); + product.setMerchant("Seller1"); + + Offer offer = new Offer(); + offer.setOfferId("68735"); + offer.setLastUpdate("2020-07-07"); + offer.setSourceProduct(product); + offer.setTargetProduct(product); + + // Act + OfferDTO dto = OfferMapper.mapOfferToDTO(offer); + + // Assert + assertEquals("68735", dto.getOfferId()); + assertEquals("2020-07-07", dto.getLastUpdate()); + + } + + } diff --git a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java index bb16c4b..5982b83 100644 --- a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java +++ b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java @@ -13,7 +13,7 @@ import de.rwu.easydrop.exception.InvalidOfferException; import de.rwu.easydrop.model.Offer; -public class OfferValidatorTest { +class OfferValidatorTest { @Test void validate_Offer_ValidProduct_NoExceptionThrown() { diff --git a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java index 70bf52a..62ef077 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java @@ -16,7 +16,7 @@ import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.Webshop; -public class OfferWriterTest { +class OfferWriterTest { @Mock private OfferDTO offerDTO; From 9a9dfe92604a48b4fc105e3140862e623025ce00 Mon Sep 17 00:00:00 2001 From: Alexander Maier Date: Tue, 27 Jun 2023 19:54:58 +0200 Subject: [PATCH 61/71] checkstyle + added new folder for processing tests --- .../de/rwu/easydrop/service/mapping/OfferMapperTest.java | 1 - .../rwu/easydrop/service/processing/OfferIdentifierTest.java | 5 +++++ .../easydrop/service/processing/OfferProvisionerTest.java | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java diff --git a/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java index 099d63c..178588d 100644 --- a/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java +++ b/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java @@ -59,5 +59,4 @@ class OfferMapperTest { } - } diff --git a/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java b/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java new file mode 100644 index 0000000..cde6ca6 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java @@ -0,0 +1,5 @@ +package de.rwu.easydrop.service.processing; + +public class OfferIdentifierTest { + +} diff --git a/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java b/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java new file mode 100644 index 0000000..c3f2fd9 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java @@ -0,0 +1,5 @@ +package de.rwu.easydrop.service.processing; + +public class OfferProvisionerTest { + +} From a2337ed3ad31ad2bccd81b17d40988bf38d20d31 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 00:38:02 +0200 Subject: [PATCH 62/71] #75 Added transaction logic --- src/main/java/de/rwu/easydrop/core/Core.java | 13 +- .../connector/OfferPersistenceInterface.java | 25 +- .../ProductPersistenceInterface.java | 4 +- .../data/connector/SQLiteConnector.java | 244 ++++++++++++++++-- .../TransactionPersistenceInterface.java | 32 +++ .../exception/InvalidOfferException.java | 5 + .../InvalidTransactionException.java | 27 ++ .../processing/TransactionHandler.java | 47 ++++ .../validation/TransactionValidator.java | 42 +++ .../service/writer/TransactionWriter.java | 33 +++ 10 files changed, 439 insertions(+), 33 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/data/connector/TransactionPersistenceInterface.java create mode 100644 src/main/java/de/rwu/easydrop/exception/InvalidTransactionException.java create mode 100644 src/main/java/de/rwu/easydrop/service/processing/TransactionHandler.java create mode 100644 src/main/java/de/rwu/easydrop/service/validation/TransactionValidator.java create mode 100644 src/main/java/de/rwu/easydrop/service/writer/TransactionWriter.java diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java index a33c14e..6e6111b 100644 --- a/src/main/java/de/rwu/easydrop/core/Core.java +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -9,13 +9,17 @@ import org.slf4j.LoggerFactory; import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.api.dto.OfferDTO; import de.rwu.easydrop.data.connector.OfferPersistenceInterface; import de.rwu.easydrop.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.data.connector.SQLiteConnector; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.mapping.OfferMapper; import de.rwu.easydrop.service.processing.OfferIdentifier; import de.rwu.easydrop.service.processing.OfferProvisioner; +import de.rwu.easydrop.service.processing.TransactionHandler; import de.rwu.easydrop.service.retriever.CatalogueRetriever; import de.rwu.easydrop.service.retriever.ProductRetriever; import de.rwu.easydrop.service.writer.CatalogueWriter; @@ -57,6 +61,7 @@ public final class Core { ProductRetriever retriever = new ProductRetriever(dataSourceFactory, pdb); CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); CatalogueWriter catWriter = new CatalogueWriter(pdb); + TransactionPersistenceInterface tpi = new SQLiteConnector(new SQLiteDataSource()); LOGGER.info("Loading catalogues"); catRetriever.loadCatalogues(); @@ -70,8 +75,12 @@ public final class Core { provis.runProvisioner(identifiedOffers); LOGGER.info("Creating transactions"); - // Transaction logic! + TransactionHandler txHandler = new TransactionHandler(tpi); + for (OfferDTO o : odb.getOffers()) { + txHandler.turnOfferToTransaction(OfferMapper.mapOfferFromDTO(o)); + } - LOGGER.info("Done!"); + tpi.outputTransactionsToLog(); + tpi.outputSummaryToLog(); } } diff --git a/src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java b/src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java index 5d2284b..d0f04d8 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java +++ b/src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java @@ -1,7 +1,14 @@ package de.rwu.easydrop.data.connector; +import java.util.List; + import de.rwu.easydrop.api.dto.OfferDTO; +/** + * Interface to offer persistence. + * + * @since 0.3.0 + */ public interface OfferPersistenceInterface { /** * Writes a ProductDTO to persistence. @@ -19,7 +26,21 @@ public interface OfferPersistenceInterface { OfferDTO getOfferDTOById(String offerId); /** - * Deletes all data from persistence. + * Gets all offerDTOs from persistence. + * + * @return offerDTOs */ - void clearData(); + List getOffers(); + + /** + * Deletes all offer data from persistence. + */ + void clearOfferData(); + + /** + * Deletes an offer from persistence. + * + * @param offerId + */ + void deleteOfferById(String offerId); } diff --git a/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java b/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java index b5a1c1a..45ec2bd 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java +++ b/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java @@ -24,7 +24,7 @@ public interface ProductPersistenceInterface { ProductDTO getProductDTOById(String productId); /** - * Deletes all data from persistence. + * Deletes all product data from persistence. */ - void clearData(); + void clearProductData(); } diff --git a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java index 4a9e492..c38d63b 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -5,13 +5,19 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.dto.OfferDTO; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.api.dto.TransactionDTO; import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.util.FormattingUtil; /** * Allows connecting to a SQLite Database. @@ -19,7 +25,18 @@ import de.rwu.easydrop.model.Webshop; * @since 0.2.0 */ public final class SQLiteConnector implements - ProductPersistenceInterface, OfferPersistenceInterface { + ProductPersistenceInterface, OfferPersistenceInterface, + TransactionPersistenceInterface { + /** + * Base for calculating percentages. + */ + private static final int PERCENT = 100; + + /** + * Logging instance. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(SQLiteConnector.class); + /** * SQLite Database. */ @@ -41,6 +58,11 @@ public final class SQLiteConnector implements */ private static final String LAST_UPDATE_COL_NAME = "lastUpdate"; + /** + * Name of 'offerId' column. + */ + private static final String OFFERID_COL_NAME = "offerId"; + /** * Creates instance. * @@ -89,6 +111,16 @@ public final class SQLiteConnector implements + ")"); createOffers.close(); + Statement createTransactions = connection.createStatement(); + createTransactions.execute( + "CREATE TABLE IF NOT EXISTS transactions (" + + "offerId TEXT, " + + "earnings REAL, " + + "volume REAL, " + + "transactionTime TEXT" + + ")"); + createTransactions.close(); + // Close the connection connection.close(); } catch (SQLException e) { @@ -105,7 +137,7 @@ public final class SQLiteConnector implements String query = "INSERT INTO products (" + "dataOrigin, productId, currentPrice, merchant, " + "deliveryPrice, available, lastUpdate" - + ") VALUES (" + + ") VALUES ( " + "?, ?, ?, ?, ?, ?, ?" + ")"; @@ -123,7 +155,8 @@ public final class SQLiteConnector implements statement.executeUpdate(); } catch (SQLException e) { - throw new PersistenceException("Something went wrong while saving to SQLite", e); + throw new PersistenceException( + "Something went wrong while saving product to SQLite", e); } } @@ -156,7 +189,8 @@ public final class SQLiteConnector implements } } } catch (SQLException e) { - throw new PersistenceException("Something went wrong while reading from SQLite"); + throw new PersistenceException( + "Something went wrong while reading product from SQLite"); } return dto; @@ -168,16 +202,9 @@ public final class SQLiteConnector implements * @throws SQLException */ public void clearData() { - try (Connection connection = db.getConnection(); - Statement statement = connection.createStatement()) { - - String productsQuery = "DELETE FROM products;"; - String offersQuery = "DELETE FROM offers;"; - statement.executeUpdate(productsQuery); - statement.executeUpdate(offersQuery); - } catch (SQLException e) { - throw new PersistenceException("Something went wrong while clearing the database", e); - } + clearProductData(); + clearOfferData(); + clearTXData(); } /** @@ -202,17 +229,18 @@ public final class SQLiteConnector implements ProductDTO targetProduct = dto.getTargetProduct(); statement.setString(++index, dto.getOfferId()); - statement.setString(++index, sourceProduct.getProductId()); statement.setString(++index, sourceProduct.getDataOrigin().toString()); + statement.setString(++index, sourceProduct.getProductId()); statement.setDouble(++index, sourceProduct.getCurrentPrice()); - statement.setString(++index, targetProduct.getProductId()); statement.setString(++index, targetProduct.getDataOrigin().toString()); + statement.setString(++index, targetProduct.getProductId()); statement.setDouble(++index, targetProduct.getCurrentPrice()); statement.setString(++index, dto.getLastUpdate()); statement.executeUpdate(); } catch (SQLException e) { - throw new PersistenceException("Something went wrong while saving to SQLite", e); + throw new PersistenceException( + "Something went wrong while saving offer to SQLite", e); } } @@ -234,24 +262,186 @@ public final class SQLiteConnector implements try (ResultSet resultSet = statement.executeQuery()) { if (resultSet.next()) { dto = new OfferDTO(); - ProductDTO srcProduct = new ProductDTO( - resultSet.getString("sourceId"), - Webshop.fromString(resultSet.getString("sourceWebshop"))); - srcProduct.setCurrentPrice(resultSet.getDouble("sourcePrice")); - ProductDTO targetProduct = new ProductDTO( - resultSet.getString("targetId"), - Webshop.fromString(resultSet.getString("targetWebshop"))); - srcProduct.setCurrentPrice(resultSet.getDouble("targetPrice")); - dto.setOfferId(resultSet.getString("offerId")); + ProductDTO srcProduct = getProductDTOById(resultSet.getString("sourceId")); + ProductDTO targetProduct = getProductDTOById(resultSet.getString("targetId")); + dto.setOfferId(resultSet.getString(OFFERID_COL_NAME)); dto.setSourceProduct(srcProduct); dto.setTargetProduct(targetProduct); dto.setLastUpdate(resultSet.getString(LAST_UPDATE_COL_NAME)); } } } catch (SQLException e) { - throw new PersistenceException("Something went wrong while reading from SQLite", e); + throw new PersistenceException( + "Something went wrong while reading offer from SQLite", e); } return dto; } + + @Override + public void writeTX(final TransactionDTO dto) { + String query = "INSERT INTO transactions (" + + "offerId, volume, earnings, transactionTime" + + ") VALUES (" + + "?, ?, ?, ?" + + ")"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + int index = 0; + + statement.setString(++index, dto.getOfferId()); + statement.setDouble(++index, dto.getVolume()); + statement.setDouble(++index, dto.getEarnings()); + statement.setString(++index, dto.getTransactionTime()); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while saving transaction to SQLite", e); + } + } + + @Override + public void outputTransactionsToLog() { + String query = "SELECT * FROM transactions t " + + "LEFT JOIN offers o ON t.offerId = o.offerId " + + "ORDER BY transactionTime ASC"; + StringBuilder sb = new StringBuilder(); + int index = 0; + sb.append("Transaction History:"); + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + sb.append("\n"); + sb.append(String.format("%3d", ++index)); + sb.append(String.format(" - %s - ", resultSet.getString("transactionTime"))); + sb.append(String.format("+%s (%s) - %s", + FormattingUtil.formatEuro(resultSet.getDouble("earnings")), + FormattingUtil.formatEuro(resultSet.getDouble("volume")), + resultSet.getString(OFFERID_COL_NAME))); + } + } + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while reading transaction from SQLite", e); + } + + String logString = sb.toString(); + LOGGER.info(logString); + } + + @Override + public void outputSummaryToLog() { + String query = "SELECT SUM(earnings) AS earnings, SUM(volume) AS volume FROM transactions;"; + StringBuilder sb = new StringBuilder(); + sb.append("Summary:\n"); + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + double earnings = resultSet.getDouble("earnings"); + double volume = resultSet.getDouble("volume"); + String avgMargin = String.valueOf( + Math.round(earnings / volume * PERCENT)) + "%"; + + String earningsFormatted = FormattingUtil.formatEuro(earnings); + String volumeFormatted = FormattingUtil.formatEuro(volume); + + sb.append(String.format("Total earnings: %s, ", earningsFormatted)); + sb.append(String.format("total volume: %s ", volumeFormatted)); + sb.append(String.format("(average margin %s)", avgMargin)); + } + } + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while reading transaction summary from SQLite", e); + } + + String logString = sb.toString(); + LOGGER.info(logString); + } + + @Override + public void clearTXData() { + flushTable("transactions"); + } + + @Override + public void clearOfferData() { + flushTable("offers"); + } + + @Override + public void clearProductData() { + flushTable("products"); + } + + /** + * Flushes all data from the specified table. + * + * @param table + */ + private void flushTable(final String table) { + try (Connection connection = db.getConnection(); + Statement statement = connection.createStatement()) { + if (table.matches("[\\w]+")) { + String query = "DELETE FROM " + table + ";"; + statement.executeUpdate(query); + } else { + throw new PersistenceException("Table name contains illegal characters"); + } + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while clearing the database", e); + } + } + + @Override + public void deleteOfferById(final String offerId) { + String query = "DELETE FROM offers WHERE offerID = ?;"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, offerId); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while deleting offer from SQLite", e); + } + } + + @Override + public List getOffers() { + List list = new ArrayList<>(); + String query = "SELECT * FROM offers"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + OfferDTO dto = new OfferDTO(); + ProductDTO srcProduct = getProductDTOById(resultSet.getString("sourceId")); + ProductDTO targetProduct = getProductDTOById(resultSet.getString("targetId")); + dto.setOfferId(resultSet.getString(OFFERID_COL_NAME)); + dto.setSourceProduct(srcProduct); + dto.setTargetProduct(targetProduct); + dto.setLastUpdate(resultSet.getString(LAST_UPDATE_COL_NAME)); + + list.add(dto); + } + } + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while reading offers from SQLite", e); + } + + return list; + } } diff --git a/src/main/java/de/rwu/easydrop/data/connector/TransactionPersistenceInterface.java b/src/main/java/de/rwu/easydrop/data/connector/TransactionPersistenceInterface.java new file mode 100644 index 0000000..8a930a3 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/TransactionPersistenceInterface.java @@ -0,0 +1,32 @@ +package de.rwu.easydrop.data.connector; + +import de.rwu.easydrop.api.dto.TransactionDTO; + +/** + * Persistence interface for transactions. + * + * @since 0.3.0 + */ +public interface TransactionPersistenceInterface { + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + void writeTX(TransactionDTO dto); + + /** + * Writes transactions to log. + */ + void outputTransactionsToLog(); + + /** + * Writes transaction summary (total volume and earnings) to log. + */ + void outputSummaryToLog(); + + /** + * Deletes all transaction data from persistence. + */ + void clearTXData(); +} diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java b/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java index f4d2c0b..aefe676 100644 --- a/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java +++ b/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java @@ -1,5 +1,10 @@ package de.rwu.easydrop.exception; +/** + * Exception that signifies the data of an Offer are invalid. + * + * @since 0.3.0 + */ public class InvalidOfferException extends RuntimeException { /** * Throws an exception that signifies the data of an Offer are invalid. diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidTransactionException.java b/src/main/java/de/rwu/easydrop/exception/InvalidTransactionException.java new file mode 100644 index 0000000..66442a4 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/InvalidTransactionException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies the data of a Transaction are invalid. + * + * @since 0.3.0 + */ +public class InvalidTransactionException extends RuntimeException { + /** + * Throws an exception that signifies the data of an Offer are invalid. + * + * @param message + */ + public InvalidTransactionException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of an Offer are invalid. + * + * @param message + * @param cause + */ + public InvalidTransactionException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/processing/TransactionHandler.java b/src/main/java/de/rwu/easydrop/service/processing/TransactionHandler.java new file mode 100644 index 0000000..928625c --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/processing/TransactionHandler.java @@ -0,0 +1,47 @@ +package de.rwu.easydrop.service.processing; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Transaction; +import de.rwu.easydrop.service.mapping.TransactionMapper; +import de.rwu.easydrop.util.Timestamp; + +/** + * Handles transactions. + * + * @since 0.3.0 + */ +public class TransactionHandler { + /** + * Transaction persistence. + */ + private TransactionPersistenceInterface txPersistence; + + /** + * Creates new transaction handler. + * + * @param txdb transaction persistence + */ + public TransactionHandler(final TransactionPersistenceInterface txdb) { + this.txPersistence = txdb; + } + + /** + * Creates transaction from offer. + * + * @param offer Offer + */ + public void turnOfferToTransaction(final Offer offer) { + Transaction tx = new Transaction(); + tx.setOfferId(offer.getOfferId()); + tx.setVolume(offer.getTargetProduct().getCurrentPrice()); + tx.setEarnings(offer.getTargetProduct().getCurrentPrice() + - offer.getSourceProduct().getCurrentPrice()); + tx.setTransactionTime(Timestamp.now()); + + // Write transaction to persistence + TransactionDTO txDTO = TransactionMapper.mapTXToDTO(tx); + txPersistence.writeTX(txDTO); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/validation/TransactionValidator.java b/src/main/java/de/rwu/easydrop/service/validation/TransactionValidator.java new file mode 100644 index 0000000..b73e435 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/validation/TransactionValidator.java @@ -0,0 +1,42 @@ +package de.rwu.easydrop.service.validation; + +import de.rwu.easydrop.exception.InvalidTransactionException; +import de.rwu.easydrop.model.Transaction; + +/** + * Validates transactions. + * + * @since 0.3.0 + */ +public final class TransactionValidator { + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private TransactionValidator() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a validator class, don't instantiate it."); + } + + /** + * Makes sure a transaction does not contain invalid information. + * + * @param tx the Transaction + */ + public static void validate(final Transaction tx) { + try { + if (tx.getOfferId().equals("")) { + throw new InvalidTransactionException("Offer ID cannot be empty"); + } + if (tx.getEarnings() == 0) { + throw new InvalidTransactionException("Earnings can't be zero"); + } + if (tx.getVolume() == 0) { + throw new InvalidTransactionException("Volume can't be zero"); + } + } catch (NullPointerException e) { + throw new InvalidTransactionException( + "Required information is missing in the transaction", e); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/service/writer/TransactionWriter.java b/src/main/java/de/rwu/easydrop/service/writer/TransactionWriter.java new file mode 100644 index 0000000..ff649c0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/TransactionWriter.java @@ -0,0 +1,33 @@ +package de.rwu.easydrop.service.writer; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.model.Transaction; +import de.rwu.easydrop.service.mapping.TransactionMapper; +import de.rwu.easydrop.service.validation.TransactionValidator; + +public class TransactionWriter { + /** + * Persistence. + */ + private TransactionPersistenceInterface persistence; + + /** + * @param newPersistence the persistence to set + */ + public TransactionWriter(final TransactionPersistenceInterface newPersistence) { + this.persistence = newPersistence; + } + + /** + * Validates and saves a transaction to persistence. + * + * @param tx Transaction + */ + public void writeTXToPersistence(final Transaction tx) { + TransactionDTO dto = TransactionMapper.mapTXToDTO(tx); + TransactionValidator.validate(tx); + + persistence.writeTX(dto); + } +} From bced347354fa3c94ef48501dcda77480a82d3f87 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 00:38:15 +0200 Subject: [PATCH 63/71] Added persistence to local mapping --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index fe6a261..e9048c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,3 +9,4 @@ services: image: easydrop:0.3.0-SNAPSHOT volumes: - ./config:/config + - ./persistence.db:/persistence.db From 187db7e6f501ad48de19c6396acb77503ba5e995 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 00:44:07 +0200 Subject: [PATCH 64/71] Patched potential security risk --- .../data/connector/SQLiteConnector.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java index c38d63b..fff1cc8 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -369,33 +369,28 @@ public final class SQLiteConnector implements @Override public void clearTXData() { - flushTable("transactions"); + runDeletionQuery("DELETE FROM transactions;"); } @Override public void clearOfferData() { - flushTable("offers"); + runDeletionQuery("DELETE FROM offers;"); } @Override public void clearProductData() { - flushTable("products"); + runDeletionQuery("DELETE FROM products;"); } /** - * Flushes all data from the specified table. + * Flushes all data using the specified query. * - * @param table + * @param query */ - private void flushTable(final String table) { + private void runDeletionQuery(final String query) { try (Connection connection = db.getConnection(); Statement statement = connection.createStatement()) { - if (table.matches("[\\w]+")) { - String query = "DELETE FROM " + table + ";"; - statement.executeUpdate(query); - } else { - throw new PersistenceException("Table name contains illegal characters"); - } + statement.executeUpdate(query); } catch (SQLException e) { throw new PersistenceException("Something went wrong while clearing the database", e); } From 0613e261e15cedd5a13dd99c09e98977d7fe0394 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 02:56:54 +0200 Subject: [PATCH 65/71] Added abstraction layer to move out config --- .../easydrop/api/client/PurchaserFactory.java | 55 +++++++++++++++++++ .../easydrop/api/client/SellerFactory.java | 55 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/main/java/de/rwu/easydrop/api/client/PurchaserFactory.java create mode 100644 src/main/java/de/rwu/easydrop/api/client/SellerFactory.java diff --git a/src/main/java/de/rwu/easydrop/api/client/PurchaserFactory.java b/src/main/java/de/rwu/easydrop/api/client/PurchaserFactory.java new file mode 100644 index 0000000..24242e4 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/PurchaserFactory.java @@ -0,0 +1,55 @@ +package de.rwu.easydrop.api.client; + +import javax.naming.ConfigurationException; + +import de.rwu.easydrop.util.Config; + +/** + * Factory for Buyer API accessors. + * + * @since 0.3.0 + */ +public class PurchaserFactory { + + /** + * The data source config. + */ + private Config config; + + /** + * @param newConfig the config to set + */ + public void setConfig(final Config newConfig) throws ConfigurationException { + this.config = newConfig; + this.config.loadConfig(); + } + + /** + * @param newConfig + */ + public PurchaserFactory(final Config newConfig) throws ConfigurationException { + this.setConfig(newConfig); + } + + /** + * Creates an Amazon purchaser. + * + * @return AmazonPurchaser + */ + public AmazonPurchaser createAmazonPurchaser() { + String apiUrl = config.getProperty("AMAZON_API_URL"); + String apiKey = config.getProperty("AMAZON_API_KEY"); + return new AmazonPurchaser(apiUrl, apiKey); + } + + /** + * Creates an eBay purchaser. + * + * @return EbayPurchaser + */ + public EbayPurchaser createEbayPurchaser() { + String apiUrl = config.getProperty("EBAY_API_URL"); + String apiKey = config.getProperty("EBAY_API_KEY"); + return new EbayPurchaser(apiUrl, apiKey); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/SellerFactory.java b/src/main/java/de/rwu/easydrop/api/client/SellerFactory.java new file mode 100644 index 0000000..df64a3f --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/SellerFactory.java @@ -0,0 +1,55 @@ +package de.rwu.easydrop.api.client; + +import javax.naming.ConfigurationException; + +import de.rwu.easydrop.util.Config; + +/** + * Factory for Sales API accessors. + * + * @since 0.3.0 + */ +public class SellerFactory { + + /** + * The data source config. + */ + private Config config; + + /** + * @param newConfig the config to set + */ + public void setConfig(final Config newConfig) throws ConfigurationException { + this.config = newConfig; + this.config.loadConfig(); + } + + /** + * @param newConfig + */ + public SellerFactory(final Config newConfig) throws ConfigurationException { + this.setConfig(newConfig); + } + + /** + * Creates an Amazon Seller. + * + * @return AmazonSeller + */ + public AmazonSeller createAmazonSeller() { + String apiUrl = config.getProperty("AMAZON_API_URL"); + String apiKey = config.getProperty("AMAZON_API_KEY"); + return new AmazonSeller(apiUrl, apiKey); + } + + /** + * Creates an eBay Seller. + * + * @return EbaySeller + */ + public EbaySeller createEbaySeller() { + String apiUrl = config.getProperty("EBAY_API_URL"); + String apiKey = config.getProperty("EBAY_API_KEY"); + return new EbaySeller(apiUrl, apiKey); + } +} From 6fe29e4319d4a10cf187495dadd96484147e4aaf Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 02:57:13 +0200 Subject: [PATCH 66/71] Added missing buy logic --- src/main/java/de/rwu/easydrop/core/Core.java | 26 ++++--- .../data/connector/SQLiteConnector.java | 7 +- .../service/processing/OfferProvisioner.java | 69 +++++++++++++++---- 3 files changed, 76 insertions(+), 26 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java index 6e6111b..45f01ef 100644 --- a/src/main/java/de/rwu/easydrop/core/Core.java +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -9,6 +9,8 @@ import org.slf4j.LoggerFactory; import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.api.client.PurchaserFactory; +import de.rwu.easydrop.api.client.SellerFactory; import de.rwu.easydrop.api.dto.OfferDTO; import de.rwu.easydrop.data.connector.OfferPersistenceInterface; import de.rwu.easydrop.data.connector.ProductPersistenceInterface; @@ -56,12 +58,16 @@ public final class Core { LOGGER.info("Preparing..."); DataSourceFactory dataSourceFactory = new DataSourceFactory(config); - ProductPersistenceInterface pdb = new SQLiteConnector(new SQLiteDataSource()); - OfferPersistenceInterface odb = new SQLiteConnector(new SQLiteDataSource()); - ProductRetriever retriever = new ProductRetriever(dataSourceFactory, pdb); + SellerFactory sellerFactory = new SellerFactory(config); + PurchaserFactory purchaserFactory = new PurchaserFactory(config); + + ProductPersistenceInterface productDB = new SQLiteConnector(new SQLiteDataSource()); + OfferPersistenceInterface offerDB = new SQLiteConnector(new SQLiteDataSource()); + TransactionPersistenceInterface txDB = new SQLiteConnector(new SQLiteDataSource()); + + ProductRetriever retriever = new ProductRetriever(dataSourceFactory, productDB); CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); - CatalogueWriter catWriter = new CatalogueWriter(pdb); - TransactionPersistenceInterface tpi = new SQLiteConnector(new SQLiteDataSource()); + CatalogueWriter catWriter = new CatalogueWriter(productDB); LOGGER.info("Loading catalogues"); catRetriever.loadCatalogues(); @@ -70,17 +76,17 @@ public final class Core { LOGGER.info("Creating offers"); OfferIdentifier ident = new OfferIdentifier(); + OfferProvisioner provis = new OfferProvisioner(offerDB, sellerFactory, purchaserFactory); List identifiedOffers = ident.runIdentifier(pCats); - OfferProvisioner provis = new OfferProvisioner(odb); provis.runProvisioner(identifiedOffers); LOGGER.info("Creating transactions"); - TransactionHandler txHandler = new TransactionHandler(tpi); - for (OfferDTO o : odb.getOffers()) { + TransactionHandler txHandler = new TransactionHandler(txDB); + for (OfferDTO o : offerDB.getOffers()) { txHandler.turnOfferToTransaction(OfferMapper.mapOfferFromDTO(o)); } - tpi.outputTransactionsToLog(); - tpi.outputSummaryToLog(); + txDB.outputTransactionsToLog(); + txDB.outputSummaryToLog(); } } diff --git a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java index fff1cc8..3b110b5 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -319,9 +319,12 @@ public final class SQLiteConnector implements sb.append("\n"); sb.append(String.format("%3d", ++index)); sb.append(String.format(" - %s - ", resultSet.getString("transactionTime"))); - sb.append(String.format("+%s (%s) - %s", + sb.append(String.format("+%s (%s) - ", FormattingUtil.formatEuro(resultSet.getDouble("earnings")), - FormattingUtil.formatEuro(resultSet.getDouble("volume")), + FormattingUtil.formatEuro(resultSet.getDouble("volume")))); + sb.append(String.format("[%s to %s] %s", + resultSet.getString("sourceWebshop"), + resultSet.getString("targetWebshop"), resultSet.getString(OFFERID_COL_NAME))); } } diff --git a/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java index 5c1847e..384fdc7 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java @@ -2,21 +2,27 @@ package de.rwu.easydrop.service.processing; import java.util.List; +import de.rwu.easydrop.api.client.AmazonPurchaser; import de.rwu.easydrop.api.client.AmazonSeller; +import de.rwu.easydrop.api.client.EbayPurchaser; import de.rwu.easydrop.api.client.EbaySeller; +import de.rwu.easydrop.api.client.PurchaserFactory; +import de.rwu.easydrop.api.client.SellerFactory; import de.rwu.easydrop.data.connector.OfferPersistenceInterface; import de.rwu.easydrop.model.Offer; import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.writer.OfferWriter; -import de.rwu.easydrop.util.Config; import de.rwu.easydrop.util.FormattingUtil; +import lombok.Data; +/** + * Offer provisioner. + * + * @since 0.3.0 + */ +@Data public class OfferProvisioner { - /** - * Config. - */ - private Config config; /** * Writes offers into persistence. */ @@ -30,7 +36,20 @@ public class OfferProvisioner { * Is the API for selling products on Ebay. */ private EbaySeller ebaySeller; + /** + * Is the API for buying products on Amazon. + */ + private AmazonPurchaser amazonPurchaser; + /** + * Is the API for buying products on Ebay. + */ + private EbayPurchaser ebayPurchaser; + /** + * Puts up the sell order. + * + * @param offer + */ private void toSeller(final Offer offer) throws IllegalArgumentException { if (offer.getTargetProduct().getDataOrigin() == Webshop.EBAY) { this.ebaySeller.sellProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); @@ -43,20 +62,41 @@ public class OfferProvisioner { } + /** + * Puts up the buy order. + * + * @param offer + */ + private void toBuyer(final Offer offer) throws IllegalArgumentException { + if (offer.getSourceProduct().getDataOrigin() == Webshop.EBAY) { + this.ebayPurchaser.purchaseProduct( + ProductMapper.mapProductToDTO(offer.getSourceProduct())); + + } else if (offer.getTargetProduct().getDataOrigin().equals(Webshop.AMAZON)) { + this.amazonPurchaser.purchaseProduct( + ProductMapper.mapProductToDTO(offer.getSourceProduct())); + } else { + throw new IllegalArgumentException("Unsupported target plattform"); + } + + } + /** * Is the class for placing orders on a target platform. * - * @param db Persistence Interface + * @param db Persistence Interface + * @param newSellerFactory Seller Factory + * @param newPurchaserFactory Purchaser Factory */ - public OfferProvisioner(final OfferPersistenceInterface db) { + public OfferProvisioner( + final OfferPersistenceInterface db, + final SellerFactory newSellerFactory, + final PurchaserFactory newPurchaserFactory) { this.offerWriter = new OfferWriter(db); - this.config = Config.getInstance(); - this.amazonSeller = new AmazonSeller( - config.getProperty("AMAZON_API_URL"), - config.getProperty("AMAZON_API_KEY")); - this.ebaySeller = new EbaySeller( - config.getProperty("EBAY_API_URL"), - config.getProperty("EBAY_API_KEY")); + this.ebaySeller = newSellerFactory.createEbaySeller(); + this.amazonSeller = newSellerFactory.createAmazonSeller(); + this.ebayPurchaser = newPurchaserFactory.createEbayPurchaser(); + this.amazonPurchaser = newPurchaserFactory.createAmazonPurchaser(); } /** @@ -74,6 +114,7 @@ public class OfferProvisioner { + newOffer.getTargetProduct().getProductId()); this.toSeller(newOffer); + this.toBuyer(newOffer); newOffer.setOfferId(newOfferId); offerWriter.writeOfferToPersistence(newOffer); } From f32b7fc1c7904bc251467bb76e0cc40284273f06 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 04:31:15 +0200 Subject: [PATCH 67/71] Cleanup --- .../service/processing/OfferIdentifier.java | 32 +++++++++++-------- .../service/processing/OfferProvisioner.java | 27 ++++++---------- .../service/retriever/ProductRetriever.java | 12 +++---- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java index 629d594..52e8357 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java @@ -33,21 +33,25 @@ public class OfferIdentifier { for (ProductCatalogue pCat : pCats) { // Call price finder for all catalogue - ProductPair pair = getHighestMarginProducts(pCat); - Offer possibleOffer = new Offer(); - possibleOffer.setLastUpdate(Timestamp.now()); - possibleOffer.setSourceProduct(pair.getProduct1()); - possibleOffer.setTargetProduct(pair.getProduct2()); - identifiedOffers.add(possibleOffer); + try { + ProductPair pair = getHighestMarginProducts(pCat); + Offer possibleOffer = new Offer(); + possibleOffer.setLastUpdate(Timestamp.now()); + possibleOffer.setSourceProduct(pair.getProduct1()); + possibleOffer.setTargetProduct(pair.getProduct2()); + identifiedOffers.add(possibleOffer); - double margin = pair.getProduct2().getCurrentPrice() - - pair.getProduct1().getCurrentPrice(); - String marginFormatted = FormattingUtil.formatEuro(margin); - LOGGER.info("\n Identified Offer: {} ({} to {}) with margin {} ", - pCat.getProductName(), - pair.getProduct1().getDataOrigin(), - pair.getProduct2().getDataOrigin(), - marginFormatted); + double margin = pair.getProduct2().getCurrentPrice() + - pair.getProduct1().getCurrentPrice(); + String marginFormatted = FormattingUtil.formatEuro(margin); + LOGGER.info("\n Identified Offer: {} ({} to {}) with margin {} ", + pCat.getProductName(), + pair.getProduct1().getDataOrigin(), + pair.getProduct2().getDataOrigin(), + marginFormatted); + } catch (InvalidOfferException e) { + // Don't include offer if deemed invalid + } } return identifiedOffers; diff --git a/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java index 384fdc7..de6e4eb 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java +++ b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java @@ -10,7 +10,6 @@ import de.rwu.easydrop.api.client.PurchaserFactory; import de.rwu.easydrop.api.client.SellerFactory; import de.rwu.easydrop.data.connector.OfferPersistenceInterface; import de.rwu.easydrop.model.Offer; -import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.writer.OfferWriter; import de.rwu.easydrop.util.FormattingUtil; @@ -51,15 +50,13 @@ public class OfferProvisioner { * @param offer */ private void toSeller(final Offer offer) throws IllegalArgumentException { - if (offer.getTargetProduct().getDataOrigin() == Webshop.EBAY) { - this.ebaySeller.sellProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); - - } else if (offer.getTargetProduct().getDataOrigin().equals(Webshop.AMAZON)) { - this.amazonSeller.sellProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); - } else { - throw new IllegalArgumentException("Unsupported target plattform"); + switch (offer.getTargetProduct().getDataOrigin()) { + case EBAY -> this.ebaySeller.sellProduct( + ProductMapper.mapProductToDTO(offer.getTargetProduct())); + case AMAZON -> this.amazonSeller.sellProduct( + ProductMapper.mapProductToDTO(offer.getTargetProduct())); + default -> throw new IllegalArgumentException("Unsupported source plattform"); } - } /** @@ -68,17 +65,13 @@ public class OfferProvisioner { * @param offer */ private void toBuyer(final Offer offer) throws IllegalArgumentException { - if (offer.getSourceProduct().getDataOrigin() == Webshop.EBAY) { - this.ebayPurchaser.purchaseProduct( + switch (offer.getSourceProduct().getDataOrigin()) { + case EBAY -> ebayPurchaser.purchaseProduct( ProductMapper.mapProductToDTO(offer.getSourceProduct())); - - } else if (offer.getTargetProduct().getDataOrigin().equals(Webshop.AMAZON)) { - this.amazonPurchaser.purchaseProduct( + case AMAZON -> amazonPurchaser.purchaseProduct( ProductMapper.mapProductToDTO(offer.getSourceProduct())); - } else { - throw new IllegalArgumentException("Unsupported target plattform"); + default -> throw new IllegalArgumentException("Unsupported target plattform"); } - } /** diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java index 8e42ea3..e2b4d5f 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -93,13 +93,9 @@ public class ProductRetriever { * @return Product from that data source or null if data source not available */ public Product getProductFromWebshop(final Webshop shop, final String productIdentifier) { - switch (shop) { - case AMAZON: - return getProductFromAmazon(productIdentifier); - case EBAY: - return getProductFromEbay(productIdentifier); - default: - return null; - } + return switch (shop) { + case AMAZON -> getProductFromAmazon(productIdentifier); + case EBAY -> getProductFromEbay(productIdentifier); + }; } } From 44c9e0d9e4c2c3a5347e564b8e683dd809a4c109 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 04:31:26 +0200 Subject: [PATCH 68/71] Added and rewrote tests --- .../api/client/AbstractDataWriterTest.java | 11 + .../client/AmazonProductDataSourceTest.java | 27 +++ .../api/client/PurchaserFactoryTest.java | 63 ++++++ .../api/client/SellerFactoryTest.java | 63 ++++++ .../java/de/rwu/easydrop/core/CoreTest.java | 4 - .../data/connector/SQLiteConnectorTest.java | 194 +++++++++++++++++- .../exception/InvalidOfferExceptionTest.java | 61 ++++++ .../InvalidTransactionExceptionTest.java | 61 ++++++ .../rwu/easydrop/model/ProductPairTest.java | 25 +++ .../de/rwu/easydrop/model/WebshopTest.java | 23 +++ .../mapping/TransactionMapperTest.java | 71 +++++++ .../processing/OfferIdentifierTest.java | 175 +++++++++++++++- .../processing/OfferProvisionerTest.java | 104 +++++++++- .../processing/TransactionHandlerTest.java | 50 +++++ .../retriever/ProductRetrieverTest.java | 47 +++++ .../validation/OfferValidatorTest.java | 37 +++- .../validation/TransactionValidatorTest.java | 70 +++++++ .../service/writer/TransactionWriterTest.java | 63 ++++++ .../rwu/easydrop/util/FormattingUtilTest.java | 14 ++ 19 files changed, 1145 insertions(+), 18 deletions(-) create mode 100644 src/test/java/de/rwu/easydrop/api/client/PurchaserFactoryTest.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/SellerFactoryTest.java delete mode 100644 src/test/java/de/rwu/easydrop/core/CoreTest.java create mode 100644 src/test/java/de/rwu/easydrop/exception/InvalidOfferExceptionTest.java create mode 100644 src/test/java/de/rwu/easydrop/exception/InvalidTransactionExceptionTest.java create mode 100644 src/test/java/de/rwu/easydrop/model/ProductPairTest.java create mode 100644 src/test/java/de/rwu/easydrop/model/WebshopTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/mapping/TransactionMapperTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/processing/TransactionHandlerTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/validation/TransactionValidatorTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/writer/TransactionWriterTest.java diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java index 78c610f..f7a2443 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java @@ -108,4 +108,15 @@ class AbstractDataWriterTest { mockWriter.sendPutRequest(dto, "Purchase"); }); } + + @Test + void sendPutRequest_wrongDataTarget() { + ProductDTO dto = new ProductDTO(demoProductId, Webshop.EBAY); + + DataWriterException e = assertThrows(DataWriterException.class, () -> { + writer.sendPutRequest(dto, demoProductId); + }); + + assertEquals("Product data source and target whateverId API are incompatible.", e.getMessage()); + } } diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java index ba3bfa9..14667b3 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java @@ -168,6 +168,33 @@ class AmazonProductDataSourceTest { assertEquals("Nothing found: AMAZON API responded with error code 404", exception.getMessage()); } + @Test + void testGetProductDTOById_emptyResponse() throws IOException { + // Set up the test environment + String mockResponse = ""; + + 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(Webshop.AMAZON, result.getDataOrigin()); + assertEquals(false, result.isAvailable()); + assertEquals(0.0, result.getCurrentPrice()); + assertEquals(0.0, result.getDeliveryPrice()); + assertEquals(null, result.getMerchant()); + } + @Test void testGetProductDTOById_ioException() throws IOException { // Set up the test environment diff --git a/src/test/java/de/rwu/easydrop/api/client/PurchaserFactoryTest.java b/src/test/java/de/rwu/easydrop/api/client/PurchaserFactoryTest.java new file mode 100644 index 0000000..c14285f --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/PurchaserFactoryTest.java @@ -0,0 +1,63 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.util.Config; + +class PurchaserFactoryTest { + + @Mock + private Config mockConfig; + + private PurchaserFactory purchaserFactory; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + purchaserFactory = new PurchaserFactory(mockConfig); + purchaserFactory.setConfig(mockConfig); + } + + @Test + void createAmazonPurchaser_ValidConfig_ReturnsAmazonPurchaser() { + // Arrange + String amazonApiUrl = "amazon_api_url"; + String amazonApiKey = "amazon_api_key"; + + when(mockConfig.getProperty("AMAZON_API_URL")).thenReturn(amazonApiUrl); + when(mockConfig.getProperty("AMAZON_API_KEY")).thenReturn(amazonApiKey); + + // Act + AmazonPurchaser amazonPurchaser = purchaserFactory.createAmazonPurchaser(); + + // Assert + assertNotNull(amazonPurchaser); + assertEquals(amazonApiKey, amazonPurchaser.getApiKey()); + } + + @Test + void createEbayPurchaser_ValidConfig_ReturnsEbayPurchaser() { + // Arrange + String ebayApiUrl = "ebay_api_url"; + String ebayApiKey = "ebay_api_key"; + + when(mockConfig.getProperty("EBAY_API_URL")).thenReturn(ebayApiUrl); + when(mockConfig.getProperty("EBAY_API_KEY")).thenReturn(ebayApiKey); + + // Act + EbayPurchaser ebayPurchaser = purchaserFactory.createEbayPurchaser(); + + // Assert + assertNotNull(ebayPurchaser); + assertEquals(ebayApiKey, ebayPurchaser.getApiKey()); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/SellerFactoryTest.java b/src/test/java/de/rwu/easydrop/api/client/SellerFactoryTest.java new file mode 100644 index 0000000..c3c27ee --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/SellerFactoryTest.java @@ -0,0 +1,63 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.util.Config; + +class SellerFactoryTest { + + @Mock + private Config mockConfig; + + private SellerFactory sellerFactory; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + sellerFactory = new SellerFactory(mockConfig); + sellerFactory.setConfig(mockConfig); + } + + @Test + void createAmazonSeller_ValidConfig_ReturnsAmazonSeller() { + // Arrange + String amazonApiUrl = "amazon_api_url"; + String amazonApiKey = "amazon_api_key"; + + when(mockConfig.getProperty("AMAZON_API_URL")).thenReturn(amazonApiUrl); + when(mockConfig.getProperty("AMAZON_API_KEY")).thenReturn(amazonApiKey); + + // Act + AmazonSeller amazonSeller = sellerFactory.createAmazonSeller(); + + // Assert + assertNotNull(amazonSeller); + assertEquals(amazonApiKey, amazonSeller.getApiKey()); + } + + @Test + void createEbaySeller_ValidConfig_ReturnsEbaySeller() { + // Arrange + String ebayApiUrl = "ebay_api_url"; + String ebayApiKey = "ebay_api_key"; + + when(mockConfig.getProperty("EBAY_API_URL")).thenReturn(ebayApiUrl); + when(mockConfig.getProperty("EBAY_API_KEY")).thenReturn(ebayApiKey); + + // Act + EbaySeller ebaySeller = sellerFactory.createEbaySeller(); + + // Assert + assertNotNull(ebaySeller); + assertEquals(ebayApiKey, ebaySeller.getApiKey()); + } +} diff --git a/src/test/java/de/rwu/easydrop/core/CoreTest.java b/src/test/java/de/rwu/easydrop/core/CoreTest.java deleted file mode 100644 index bf62c6d..0000000 --- a/src/test/java/de/rwu/easydrop/core/CoreTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package de.rwu.easydrop.core; - -public class CoreTest { -} diff --git a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java index 3dcfd84..68ee485 100644 --- a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java +++ b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java @@ -8,7 +8,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import org.junit.jupiter.api.BeforeEach; @@ -17,13 +21,23 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.sqlite.SQLiteDataSource; +import de.rwu.easydrop.api.dto.OfferDTO; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.api.dto.TransactionDTO; import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.model.Webshop; class SQLiteConnectorTest { private static final String TEST_PRODUCT_ID = "12345"; private SQLiteConnector sqliteConnector = new SQLiteConnector(new SQLiteDataSource()); + @Mock + private Connection connection; + + @Mock + private PreparedStatement statement; + + @Mock + private ResultSet resultSet; @Mock private SQLiteDataSource mockDataSource; @@ -135,11 +149,179 @@ class SQLiteConnectorTest { } private void insertSampleProduct() { - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON); - ProductDTO.setCurrentPrice(9.99); - ProductDTO.setMerchant("Sample Merchant"); - ProductDTO.setDeliveryPrice(2.50); - ProductDTO.setAvailable(true); - sqliteConnector.writeProduct(ProductDTO); + ProductDTO productDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON); + productDTO.setCurrentPrice(9.99); + productDTO.setMerchant("Sample Merchant"); + productDTO.setDeliveryPrice(2.50); + productDTO.setAvailable(true); + sqliteConnector.writeProduct(productDTO); + } + + private void insertSampleOffer() { + ProductDTO p1 = new ProductDTO("p1", Webshop.AMAZON); + p1.setCurrentPrice(123f); + + ProductDTO p2 = new ProductDTO("p2", Webshop.EBAY); + p2.setCurrentPrice(234f); + + OfferDTO dto = new OfferDTO(); + dto.setOfferId("123"); + dto.setLastUpdate("2020-01-01 00:00:00"); + dto.setSourceProduct(p1); + dto.setTargetProduct(p2); + sqliteConnector.writeOffer(dto); + } + + private void insertSampleTransaction() { + TransactionDTO dto = new TransactionDTO(); + dto.setOfferId("123"); + dto.setEarnings(12f); + dto.setVolume(123f); + dto.setTransactionTime("2020-01-01 00:00:00"); + sqliteConnector.writeTX(dto); + } + + @Test + void outputTransactionsToLog_NothingThrown() { + insertSampleTransaction(); + + assertDoesNotThrow(() -> { + sqliteConnector.outputTransactionsToLog(); + }); + } + + @Test + void outputSummaryToLog_NothingThrown() { + insertSampleTransaction(); + + assertDoesNotThrow(() -> { + sqliteConnector.outputSummaryToLog(); + }); + } + + @Test + void outputTransactionsToLog_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.outputTransactionsToLog()); + assertEquals("Something went wrong while reading transaction from SQLite", e.getMessage()); + } + + @Test + void outputSummaryToLog_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.outputSummaryToLog()); + assertEquals("Something went wrong while reading transaction summary from SQLite", e.getMessage()); + } + + @Test + void deleteOfferById_SuccessfulDelete() { + // Arrange + sqliteConnector.clearData(); + insertSampleOffer(); + + // Make sure it's there + assertEquals(1, sqliteConnector.getOffers().size()); + + // Act + sqliteConnector.deleteOfferById("123"); + + // Assert + assertEquals(0, sqliteConnector.getOffers().size()); + } + + @Test + void deleteOfferById_ThrowsPersistenceException_OnSQLException() throws SQLException { + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.deleteOfferById("123")); + assertEquals("Something went wrong while deleting offer from SQLite", e.getMessage()); + } + + @Test + void getOffers_ReturnsCorrectValues() { + sqliteConnector.clearData(); + assertEquals(0, sqliteConnector.getOffers().size()); + + insertSampleOffer(); + + assertEquals(1, sqliteConnector.getOffers().size()); + } + + @Test + void getOffers_ThrowsPersistenceException_OnSQLException() throws SQLException { + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.getOffers()); + assertEquals("Something went wrong while reading offers from SQLite", e.getMessage()); + } + + @Test + void writeOffer_ValidDTO_DataSavedToDatabase() throws SQLException { + // Arrange + OfferDTO offerDTO = new OfferDTO(); + offerDTO.setOfferId("123"); + offerDTO.setSourceProduct(new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON)); + offerDTO.setTargetProduct(new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON)); + + String expectedQuery = "INSERT INTO offers (offerId, sourceWebshop, sourceId, sourcePrice, " + + "targetWebshop, targetId, targetPrice, lastupdate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + + when(connection.prepareStatement(expectedQuery)).thenReturn(statement); + + // Act + assertDoesNotThrow(() -> sqliteConnector.writeOffer(offerDTO)); + } + + @Test + void writeTX_NothingThrown() { + TransactionDTO dto = new TransactionDTO(); + + assertDoesNotThrow(() -> { + sqliteConnector.writeTX(dto); + }); + } + + @Test + void writeTX_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + TransactionDTO dto = new TransactionDTO(); + + // Act and Assert + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.writeTX(dto)); + assertEquals("Something went wrong while saving transaction to SQLite", e.getMessage()); + } + + @Test + void getOfferDTOById_OfferExists_ReturnsProductDTO() { + // Arrange + sqliteConnector.clearData(); + insertSampleOffer(); + + // Act + OfferDTO offerDTO = sqliteConnector.getOfferDTOById("123"); + + // Assert + assertNotNull(offerDTO); + assertEquals("123", offerDTO.getOfferId()); + assertEquals("2020-01-01 00:00:00", offerDTO.getLastUpdate()); } } diff --git a/src/test/java/de/rwu/easydrop/exception/InvalidOfferExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/InvalidOfferExceptionTest.java new file mode 100644 index 0000000..1dfd268 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/InvalidOfferExceptionTest.java @@ -0,0 +1,61 @@ +package de.rwu.easydrop.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class InvalidOfferExceptionTest { + + @Test + void constructor_WithMessage_SetsMessage() { + // Arrange + String message = "Invalid Offer data"; + + // Act + InvalidOfferException exception = new InvalidOfferException(message); + + // Assert + assertEquals(message, exception.getMessage()); + } + + @Test + void constructor_WithMessageAndCause_SetsMessageAndCause() { + // Arrange + String message = "Invalid Offer data"; + Throwable cause = new IllegalArgumentException("Invalid argument"); + + // Act + InvalidOfferException exception = new InvalidOfferException(message, cause); + + // Assert + assertEquals(message, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } + + @Test + void constructor_WithNullMessage_SetsNullMessage() { + // Act + InvalidOfferException exception = new InvalidOfferException(null); + + // Assert + assertEquals(null, exception.getMessage()); + } + + @Test + void constructor_WithNullCause_SetsNullCause() { + // Act + InvalidOfferException exception = new InvalidOfferException("Invalid Offer data", null); + + // Assert + assertEquals(null, exception.getCause()); + } + + @Test + void throw_InvalidOfferException() { + // Act and Assert + assertThrows(InvalidOfferException.class, () -> { + throw new InvalidOfferException("Invalid Offer data"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/exception/InvalidTransactionExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/InvalidTransactionExceptionTest.java new file mode 100644 index 0000000..32e4971 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/InvalidTransactionExceptionTest.java @@ -0,0 +1,61 @@ +package de.rwu.easydrop.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class InvalidTransactionExceptionTest { + + @Test + void constructor_WithMessage_SetsMessage() { + // Arrange + String message = "Invalid Transaction data"; + + // Act + InvalidTransactionException exception = new InvalidTransactionException(message); + + // Assert + assertEquals(message, exception.getMessage()); + } + + @Test + void constructor_WithMessageAndCause_SetsMessageAndCause() { + // Arrange + String message = "Invalid Transaction data"; + Throwable cause = new IllegalArgumentException("Invalid argument"); + + // Act + InvalidTransactionException exception = new InvalidTransactionException(message, cause); + + // Assert + assertEquals(message, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } + + @Test + void constructor_WithNullMessage_SetsNullMessage() { + // Act + InvalidTransactionException exception = new InvalidTransactionException(null); + + // Assert + assertEquals(null, exception.getMessage()); + } + + @Test + void constructor_WithNullCause_SetsNullCause() { + // Act + InvalidTransactionException exception = new InvalidTransactionException("Invalid Transaction data", null); + + // Assert + assertEquals(null, exception.getCause()); + } + + @Test + void throw_InvalidTransactionException() { + // Act and Assert + assertThrows(InvalidTransactionException.class, () -> { + throw new InvalidTransactionException("Invalid Transaction data"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/model/ProductPairTest.java b/src/test/java/de/rwu/easydrop/model/ProductPairTest.java new file mode 100644 index 0000000..b562918 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/model/ProductPairTest.java @@ -0,0 +1,25 @@ +package de.rwu.easydrop.model; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +class ProductPairTest { + + @Test + void constructor_TwoProducts_ProductsInitializedCorrectly() { + // Arrange + Product product1 = new Product(); + product1.setProductId("123"); + + Product product2 = new Product(); + product2.setProductId("234"); + + // Act + ProductPair pair = new ProductPair(product1, product2); + + // Assert + assertSame(product1, pair.getProduct1()); + assertSame(product2, pair.getProduct2()); + } +} diff --git a/src/test/java/de/rwu/easydrop/model/WebshopTest.java b/src/test/java/de/rwu/easydrop/model/WebshopTest.java new file mode 100644 index 0000000..6e40870 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/model/WebshopTest.java @@ -0,0 +1,23 @@ +package de.rwu.easydrop.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class WebshopTest { + + @Test + void testFromString_returnsIntendedConstant() { + Webshop testShop = Webshop.fromString("Amazon"); + + assertEquals(Webshop.AMAZON, testShop); + } + + @Test + void testFromString_invalidShop() { + assertThrows(IllegalArgumentException.class, () -> { + Webshop.fromString("thisdoesnotexist"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/mapping/TransactionMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/TransactionMapperTest.java new file mode 100644 index 0000000..79de525 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/mapping/TransactionMapperTest.java @@ -0,0 +1,71 @@ +package de.rwu.easydrop.service.mapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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.Test; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.model.Transaction; + +class TransactionMapperTest { + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = TransactionMapper.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + @Test + void mapTXFromDTO_WithValidTransactionDTO_ReturnsTransaction() { + // Arrange + TransactionDTO transactionDTO = new TransactionDTO(); + transactionDTO.setOfferId("123"); + transactionDTO.setVolume(10.0); + transactionDTO.setEarnings(5.0); + transactionDTO.setTransactionTime("2023-06-25 03:26:59"); + + // Act + Transaction transaction = TransactionMapper.mapTXFromDTO(transactionDTO); + + // Assert + assertNotNull(transaction); + assertEquals(transactionDTO.getOfferId(), transaction.getOfferId()); + assertEquals(transactionDTO.getVolume(), transaction.getVolume()); + assertEquals(transactionDTO.getEarnings(), transaction.getEarnings()); + assertEquals(transactionDTO.getTransactionTime(), transaction.getTransactionTime()); + } + + @Test + void mapTXToDTO_WithValidTransaction_ReturnsTransactionDTO() { + // Arrange + Transaction transaction = new Transaction(); + transaction.setOfferId("123"); + transaction.setVolume(10.0); + transaction.setEarnings(5.0); + transaction.setTransactionTime("2023-06-25 03:26:59"); + + // Act + TransactionDTO transactionDTO = TransactionMapper.mapTXToDTO(transaction); + + // Assert + assertNotNull(transactionDTO); + assertEquals(transaction.getOfferId(), transactionDTO.getOfferId()); + assertEquals(transaction.getVolume(), transactionDTO.getVolume()); + assertEquals(transaction.getEarnings(), transactionDTO.getEarnings()); + assertEquals(transaction.getTransactionTime(), transactionDTO.getTransactionTime()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java b/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java index cde6ca6..466bd56 100644 --- a/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java +++ b/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java @@ -1,5 +1,176 @@ package de.rwu.easydrop.service.processing; -public class OfferIdentifierTest { - +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.exception.InvalidCatalogueException; +import de.rwu.easydrop.exception.InvalidOfferException; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.ProductPair; + +class OfferIdentifierTest { + + @Mock + private ProductCatalogue mockProductCatalogue; + + private OfferIdentifier offerIdentifier; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + offerIdentifier = new OfferIdentifier(); + } + + @Test + void runIdentifier_WithValidProductCatalogues_IdentifiesOffers() { + // Arrange + List productCatalogues = new ArrayList<>(); + ProductCatalogue productCatalogue1 = new ProductCatalogue("Catalogue1", "desc"); + ProductCatalogue productCatalogue2 = new ProductCatalogue("Catalogue2", "desc"); + + Product product1 = new Product(); + product1.setCurrentPrice(100.0); + Product product2 = new Product(); + product2.setCurrentPrice(200.0); + Product product3 = new Product(); + product3.setCurrentPrice(300.0); + Product product4 = new Product(); + product4.setCurrentPrice(400.0); + + productCatalogue1.addProduct(product1); + productCatalogue1.addProduct(product2); + productCatalogue2.addProduct(product3); + productCatalogue2.addProduct(product4); + + productCatalogues.add(productCatalogue1); + productCatalogues.add(productCatalogue2); + + // Act + List identifiedOffers = offerIdentifier.runIdentifier(productCatalogues); + + // Assert + assertEquals(2, identifiedOffers.size()); + assertNotNull(identifiedOffers.get(0).getSourceProduct()); + assertNotNull(identifiedOffers.get(0).getTargetProduct()); + assertNotNull(identifiedOffers.get(1).getSourceProduct()); + assertNotNull(identifiedOffers.get(1).getTargetProduct()); + } + + @Test + void runIdentifier_WithOneGoodOneBadCatalogue_BadOneIsntIncluded() { + // Arrange + List productCatalogues = new ArrayList<>(); + ProductCatalogue productCatalogue1 = new ProductCatalogue("Catalogue1", "desc"); + ProductCatalogue productCatalogue2 = new ProductCatalogue("Catalogue2", "desc"); + + Product product1 = new Product(); + product1.setCurrentPrice(100.0); + Product product2 = new Product(); + product2.setCurrentPrice(200.0); + Product product3 = new Product(); + product3.setCurrentPrice(300.0); + Product product4 = new Product(); + product4.setCurrentPrice(300.0); + + productCatalogue1.addProduct(product1); + productCatalogue1.addProduct(product2); + productCatalogue2.addProduct(product3); + productCatalogue2.addProduct(product4); + + productCatalogues.add(productCatalogue1); + productCatalogues.add(productCatalogue2); + + // Act + List identifiedOffers = offerIdentifier.runIdentifier(productCatalogues); + + // Assert + assertEquals(1, identifiedOffers.size()); // Only 1 passed + } + + @Test + void getHighestMarginProducts_WithValidProductCatalogue_ReturnsProductPair() { + // Arrange + ProductCatalogue productCatalogue = new ProductCatalogue("TestCatalogue", "desc"); + Product product1 = new Product(); + product1.setCurrentPrice(100.0); + Product product2 = new Product(); + product2.setCurrentPrice(200.0); + productCatalogue.addProduct(product1); + productCatalogue.addProduct(product2); + + // Act + ProductPair productPair = OfferIdentifier.getHighestMarginProducts(productCatalogue); + + // Assert + assertNotNull(productPair); + assertNotNull(productPair.getProduct1()); + assertNotNull(productPair.getProduct2()); + assertNotEquals(productPair.getProduct1(), productPair.getProduct2()); + } + + @Test + void getHighestMarginProducts_SecondProductCheaper() { + // Arrange + ProductCatalogue productCatalogue = new ProductCatalogue("TestCatalogue", "desc"); + Product product1 = new Product(); + product1.setCurrentPrice(200.0); + Product product2 = new Product(); + product2.setCurrentPrice(100.0); + productCatalogue.addProduct(product1); + productCatalogue.addProduct(product2); + + // Act + ProductPair productPair = OfferIdentifier.getHighestMarginProducts(productCatalogue); + + // Assert + assertNotNull(productPair); + assertNotNull(productPair.getProduct1()); + assertNotNull(productPair.getProduct2()); + assertNotEquals(productPair.getProduct1(), productPair.getProduct2()); + } + + @Test + void getHighestMarginProducts_WithCatalogueSizeLessThan2_ThrowsInvalidCatalogueException() { + // Arrange + ProductCatalogue productCatalogue = new ProductCatalogue("SmallCatalogue", "desc"); + + // Act and Assert + assertThrows(InvalidCatalogueException.class, () -> { + OfferIdentifier.getHighestMarginProducts(productCatalogue); + }); + } + + @Test + void getHighestMarginProducts_WithZeroPriceMargin_ThrowsInvalidOfferException() { + // Arrange + ProductCatalogue productCatalogue = new ProductCatalogue("name", "desc"); + Product product1 = new Product(); + product1.setCurrentPrice(200.0); + + Product product2 = new Product(); + product2.setCurrentPrice(200.0); + + productCatalogue.getProducts().add(product1); + productCatalogue.getProducts().add(product2); + + // Act and Assert + InvalidOfferException e = assertThrows(InvalidOfferException.class, () -> { + OfferIdentifier.getHighestMarginProducts(productCatalogue); + }); + + assertEquals("Price margin is zero!", e.getMessage()); + } } diff --git a/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java b/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java index c3f2fd9..3b3df76 100644 --- a/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java +++ b/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java @@ -1,5 +1,105 @@ package de.rwu.easydrop.service.processing; -public class OfferProvisionerTest { - +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.client.AmazonPurchaser; +import de.rwu.easydrop.api.client.AmazonSeller; +import de.rwu.easydrop.api.client.EbayPurchaser; +import de.rwu.easydrop.api.client.EbaySeller; +import de.rwu.easydrop.api.client.PurchaserFactory; +import de.rwu.easydrop.api.client.SellerFactory; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.service.writer.OfferWriter; + +class OfferProvisionerTest { + + @Mock + private OfferPersistenceInterface mockPersistence; + + @Mock + private SellerFactory mockSellerFactory; + + @Mock + private PurchaserFactory mockPurchaserFactory; + + @Mock + private OfferWriter mockOfferWriter; + + @Mock + private AmazonSeller mockAmazonSeller; + + @Mock + private EbaySeller mockEbaySeller; + + @Mock + private AmazonPurchaser mockAmazonPurchaser; + + @Mock + private EbayPurchaser mockEbayPurchaser; + + private OfferProvisioner offerProvisioner; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + when(mockSellerFactory.createAmazonSeller()).thenReturn(mockAmazonSeller); + when(mockSellerFactory.createEbaySeller()).thenReturn(mockEbaySeller); + when(mockPurchaserFactory.createAmazonPurchaser()).thenReturn(mockAmazonPurchaser); + when(mockPurchaserFactory.createEbayPurchaser()).thenReturn(mockEbayPurchaser); + + offerProvisioner = new OfferProvisioner(mockPersistence, mockSellerFactory, mockPurchaserFactory); + offerProvisioner.setOfferWriter(mockOfferWriter); + } + + @Test + void runProvisioner_WithValidOffers_PlacesOrders() { + // Arrange + List offers = new ArrayList<>(); + Offer offer1 = createOffer(Webshop.AMAZON, "ASIN123", Webshop.EBAY, "12345"); + Offer offer2 = createOffer(Webshop.EBAY, "54321", Webshop.AMAZON, "ASIN456"); + offers.add(offer1); + offers.add(offer2); + + // Act + offerProvisioner.runProvisioner(offers); + + // Assert + verify(mockAmazonSeller, times(1)).sellProduct(any()); + verify(mockEbaySeller, times(1)).sellProduct(any()); + verify(mockAmazonPurchaser, times(1)).purchaseProduct(any()); + verify(mockEbayPurchaser, times(1)).purchaseProduct(any()); + verify(mockOfferWriter, times(2)).writeOfferToPersistence(any()); + } + + private Offer createOffer(Webshop sourceWebshop, String sourceProductId, Webshop targetWebshop, + String targetProductId) { + Product sourceProduct = new Product(); + sourceProduct.setDataOrigin(sourceWebshop); + sourceProduct.setProductId(sourceProductId); + + Product targetProduct = new Product(); + targetProduct.setDataOrigin(targetWebshop); + targetProduct.setProductId(targetProductId); + + Offer offer = new Offer(); + offer.setSourceProduct(sourceProduct); + offer.setTargetProduct(targetProduct); + + return offer; + } } diff --git a/src/test/java/de/rwu/easydrop/service/processing/TransactionHandlerTest.java b/src/test/java/de/rwu/easydrop/service/processing/TransactionHandlerTest.java new file mode 100644 index 0000000..4527551 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/processing/TransactionHandlerTest.java @@ -0,0 +1,50 @@ +package de.rwu.easydrop.service.processing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Transaction; +import de.rwu.easydrop.service.mapping.TransactionMapper; + +class TransactionHandlerTest { + + @Mock + private TransactionPersistenceInterface mockPersistence; + + public TransactionHandlerTest() { + MockitoAnnotations.openMocks(this); + } + + @Test + void turnOfferToTransaction_ValidOffer_TransactionCreatedAndWrittenToPersistence() { + // Arrange + Offer offer = new Offer(); + Product product = new Product(); + offer.setSourceProduct(product); + offer.setTargetProduct(product); + + TransactionHandler handler = new TransactionHandler(mockPersistence); + + // Act + handler.turnOfferToTransaction(offer); + + // Assert + ArgumentCaptor transactionDtoCaptor = ArgumentCaptor.forClass(TransactionDTO.class); + verify(mockPersistence, times(1)).writeTX(transactionDtoCaptor.capture()); + + TransactionDTO capturedTransactionDto = transactionDtoCaptor.getValue(); + Transaction capturedTransaction = TransactionMapper.mapTXFromDTO(capturedTransactionDto); + + assertEquals(offer.getOfferId(), capturedTransaction.getOfferId()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java index da42da6..5d0a2db 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java @@ -2,6 +2,8 @@ package de.rwu.easydrop.service.retriever; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -108,4 +110,49 @@ class ProductRetrieverTest { // Verify the interactions verify(persistence, times(1)).getProductDTOById(productId); } + + @Test + void getProductFromWebshop_ValidAmazonWebshop_ReturnsProduct() { + // Arrange + String productIdentifier = "ASIN1"; + ProductDTO dto = new ProductDTO(productIdentifier, Webshop.AMAZON); + dto.setCurrentPrice(1.23); + AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + Product expectedProduct = new Product(); + expectedProduct.setProductId(productIdentifier); + expectedProduct.setDataOrigin(Webshop.AMAZON); + expectedProduct.setCurrentPrice(1.23); + when(dataSource.getProductDTOById(productIdentifier)).thenReturn(dto); + when(dataSourceFactory.createAmazonProductDataSource()).thenReturn(dataSource); + + // Act + Product actualProduct = productRetriever.getProductFromWebshop(Webshop.AMAZON, productIdentifier); + + // Assert + assertEquals(expectedProduct, actualProduct); + verify(dataSourceFactory).createAmazonProductDataSource(); + verify(dataSource).getProductDTOById(productIdentifier); + } + + @Test + void getProductFromWebshop_ValidEbayWebshop_ReturnsProduct() { + // Arrange + String productIdentifier = "ASIN1"; + ProductDTO dto = new ProductDTO(productIdentifier, Webshop.EBAY); + dto.setCurrentPrice(1.23); + Product expectedProduct = new Product(); + expectedProduct.setProductId(productIdentifier); + expectedProduct.setDataOrigin(Webshop.EBAY); + expectedProduct.setCurrentPrice(1.23); + when(ebayDataSource.getProductDTOById(productIdentifier)).thenReturn(dto); + when(dataSourceFactory.createEbayItemDataSource()).thenReturn(ebayDataSource); + + // Act + Product actualProduct = productRetriever.getProductFromWebshop(Webshop.EBAY, productIdentifier); + + // Assert + assertEquals(expectedProduct, actualProduct); + verify(dataSourceFactory).createEbayItemDataSource(); + verify(ebayDataSource).getProductDTOById(productIdentifier); + } } diff --git a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java index 5982b83..d9271de 100644 --- a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java +++ b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java @@ -1,8 +1,13 @@ package de.rwu.easydrop.service.validation; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; 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 java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -12,8 +17,20 @@ import org.junit.jupiter.params.provider.MethodSource; import de.rwu.easydrop.exception.InvalidOfferException; import de.rwu.easydrop.model.Offer; - class OfferValidatorTest { + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = OfferValidator.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } @Test void validate_Offer_ValidProduct_NoExceptionThrown() { @@ -26,6 +43,18 @@ class OfferValidatorTest { assertDoesNotThrow(() -> OfferValidator.validate(offer)); } + @Test + void validate_Offer_NullPointer() { + // Arrange + Offer offer = new Offer(); + + InvalidOfferException e = assertThrows(InvalidOfferException.class, () -> { + OfferValidator.validate(offer); + }); + + assertEquals("Required information is missing in the offer", e.getMessage()); + } + @ParameterizedTest @MethodSource("invalidOfferProvider") void validate_InvalidOffer_ThrowsInvalidOfferException(Offer offer) { @@ -36,8 +65,8 @@ class OfferValidatorTest { static Stream invalidOfferProvider() { return Stream.of( - createOfferWithEmptylastUpdate(), - createOfferWithEmptyId()); + createOfferWithEmptylastUpdate(), + createOfferWithEmptyId()); } private static Offer createOfferWithEmptylastUpdate() { @@ -53,5 +82,5 @@ class OfferValidatorTest { offer.setLastUpdate("8798476"); return offer; } - + } diff --git a/src/test/java/de/rwu/easydrop/service/validation/TransactionValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/TransactionValidatorTest.java new file mode 100644 index 0000000..939379e --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/validation/TransactionValidatorTest.java @@ -0,0 +1,70 @@ +package de.rwu.easydrop.service.validation; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.Test; + +import de.rwu.easydrop.exception.InvalidTransactionException; +import de.rwu.easydrop.model.Transaction; + +class TransactionValidatorTest { + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = TransactionValidator.class + .getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + @Test + void validate_ValidTransaction_NoExceptionThrown() { + // Arrange + Transaction transaction = new Transaction(); + transaction.setOfferId("123"); + transaction.setVolume(123f); + transaction.setEarnings(12.23); + transaction.setTransactionTime("2020-01-01 00:00:00"); + + // Act & Assert + assertDoesNotThrow(() -> TransactionValidator.validate(transaction)); + } + + @Test + void validate_InvalidTransaction_InvalidTransactionExceptionThrown() { + // Arrange + Transaction tx1 = new Transaction(); + tx1.setOfferId(""); // Set an empty Offer ID to make it invalid + Transaction tx2 = new Transaction(); + tx2.setOfferId("123"); + Transaction tx3 = new Transaction(); + tx3.setOfferId("123"); + tx3.setEarnings(123f); + + // Act & Assert + assertThrows(InvalidTransactionException.class, () -> TransactionValidator.validate(tx1)); + assertThrows(InvalidTransactionException.class, () -> TransactionValidator.validate(tx2)); + assertThrows(InvalidTransactionException.class, () -> TransactionValidator.validate(tx3)); + } + + @Test + void validate_NullTransaction_InvalidTransactionExceptionThrown() { + // Arrange + Transaction transaction = null; + + // Act & Assert + assertThrows(InvalidTransactionException.class, () -> TransactionValidator.validate(transaction)); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/writer/TransactionWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/TransactionWriterTest.java new file mode 100644 index 0000000..ef9dd48 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/writer/TransactionWriterTest.java @@ -0,0 +1,63 @@ +package de.rwu.easydrop.service.writer; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.exception.InvalidTransactionException; +import de.rwu.easydrop.model.Transaction; +import de.rwu.easydrop.service.mapping.TransactionMapper; + +class TransactionWriterTest { + + @Mock + private TransactionPersistenceInterface persistence; + + private TransactionWriter writer; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + writer = new TransactionWriter(persistence); + } + + @Test + void writeTXToPersistence_ValidTransaction_TransactionSaved() { + // Arrange + Transaction transaction = new Transaction(); + transaction.setOfferId("123"); + transaction.setVolume(123f); + transaction.setEarnings(12.23); + transaction.setTransactionTime("2020-01-01 00:00:00"); + + TransactionDTO expectedDTO = TransactionMapper.mapTXToDTO(transaction); + + // Act + writer.writeTXToPersistence(transaction); + + // Assert + verify(persistence).writeTX(expectedDTO); + } + + @Test + void writeTXToPersistence_InvalidTransaction_ValidationExceptionThrown() { + // Arrange + Transaction transaction = new Transaction(); + transaction.setVolume(123f); + transaction.setEarnings(12.23); + transaction.setTransactionTime("2020-01-01 00:00:00"); + + // Act & Assert + assertThrows(InvalidTransactionException.class, () -> writer.writeTXToPersistence(transaction)); + + // Verify that persistence.writeTX is not called + verifyNoInteractions(persistence); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java index 4b1dfa1..cd1d9b9 100644 --- a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java +++ b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java @@ -1,5 +1,6 @@ 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.assertTrue; @@ -55,4 +56,17 @@ class FormattingUtilTest { Assertions.assertEquals(expectedFormattedAmount, formattedAmount); } + + @Test + void testRemoveSpaces_RemovesAllSpaces() { + String test1 = FormattingUtil.removeSpaces("this is a test"); + String test2 = FormattingUtil.removeSpaces("another test"); + String test3 = FormattingUtil.removeSpaces(" daring today, aren't we? "); + String test4 = FormattingUtil.removeSpaces("hehe"); + + assertEquals("thisisatest", test1); + assertEquals("anothertest", test2); + assertEquals("daringtoday,aren'twe?", test3); + assertEquals("hehe", test4); + } } From 9f7f9edeb39ea121ce73753e298505140c6c7ed0 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 04:45:35 +0200 Subject: [PATCH 69/71] Updated changelog --- CHANGELOG.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef2078..6994ab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,13 @@ - Script for local UML generation (#56) - Transaction fulfillment API classes (#71) - - `AmazonSeller` - - `AmazonPurchaser` - - `EbaySeller` - - `EbayPurchaser` -- Dockerfile and `docker-compose.yml` +- Offer handling classes (#12, #64, #70) +- Transaction handling classes (#75) +- Dockerfile and `docker-compose.yml` (#73) + +### Changed + +- Webshops are now handled via enum instead of being hard-coded ## 0.2.0 From 85c8fe6b1af4274e752760e3eb7f63db59f81ff7 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 05:05:29 +0200 Subject: [PATCH 70/71] Added more demo products --- config/demo.products-config.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/config/demo.products-config.json b/config/demo.products-config.json index 84a4f91..0474928 100644 --- a/config/demo.products-config.json +++ b/config/demo.products-config.json @@ -11,6 +11,30 @@ "EBAY": "Gigabyte GeForce RTX 3060" } ] + }, + { + "name": "Gigabyte Radeon RX 6700 XT", + "description": "Another one", + "identifiers": [ + { + "AMAZON": "B08Y758F6C" + }, + { + "EBAY": "Gigabyte Radeon RX 6700 XT" + } + ] + }, + { + "name": "NVIDIA GeForce RTX 4060 Ti", + "description": "Premium!", + "identifiers": [ + { + "AMAZON": "B0C4F9HCF6" + }, + { + "EBAY": "NVIDIA GeForce RTX 4060 Ti" + } + ] } ] } From c74205663a2198cdf81d93ebb7889e9b0075a104 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 28 Jun 2023 05:09:04 +0200 Subject: [PATCH 71/71] Bump 1.0.0, updated docs --- CHANGELOG.md | 2 +- Dockerfile | 2 +- Docs/DemoImage.png | Bin 23760 -> 77787 bytes README.md | 6 +----- docker-compose.yml | 2 +- pom.xml | 2 +- 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6994ab2..1ff02be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.3.0-SNAPSHOT +## 1.0.0 ### Added diff --git a/Dockerfile b/Dockerfile index 3939c20..dc39d04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ RUN mvn -B package -DskipTests FROM openjdk:17-jdk-slim # Copy the jar file from the build stage -COPY --from=build /app/target/easydrop-0.3.0-SNAPSHOT.jar /easydrop.jar +COPY --from=build /app/target/easydrop-1.0.0.jar /easydrop.jar # Execute the application when the docker container starts. ENTRYPOINT ["java", "-jar", "/easydrop.jar"] diff --git a/Docs/DemoImage.png b/Docs/DemoImage.png index 18fb11fc7985ee0aa591f2f5bd039208ebe1c997..d672e79cd2b8af55a51f3100e7a908250877af16 100644 GIT binary patch literal 77787 zcmb@ucQoAZ*ZxZc(Ie3XK_puAmMEhW(R;K}B18#6q8pt=4?^_bONib^Cq!>iM~O~G z8%#fwd_K?fe9t=T{Lb^o*=t$WSj!)C-|yLbUwgl<`Jk(s7FExsfxNyJlJ@wTT@oL8J?c-kDbWqSzz{9IcAVb*@;$D-utH1EX!=vo` z^MgO+R`wDP4_o|9S;5f9>Znuex#j2?0OS9}!Cf^I@G21*aTt+BHWXrVa`TP?W%O5) zXk}$$N=5nbxUba8cOGykBq>h@lk)C$%ew1~`yYU0VP>6vu$dqcC%+T+S62s(ZI!ra zt7ykS+`w26FXw;8mM{l%HRFFq0^OqH*zkXZPs6aPv+94uV4;eHnW)Hr1ncAaDMYw% z*W0S!ZIk7`3ttDOt%k*V!6 zc0w-^l+mqCi1!GU#{d(AY%d1J2V^5Pkd`WadSLxdTT2UUbp9MVH*4zW`}h40e!VSb z<{$wolkN=jLn<}C!cM?f+>-eY*6DJdxVDb6OcJHVB`2Ky_-PK?K)UXiAFfp-kG(pU zmAeT6Ts_Nv*LJ`v8xnjVns=c+4{<0IZm+o3tF)+Pe*EaNiDjkz)w!qp53KjsQgyl ztlL$iy^fV^HwsF>U$`BGzyrAfPtUI`4_WZW?BW((-%U?}z30YWn~%lnZuljhA|jIZ zmpQE@zCSOqXP8KJXlT9ohLl7Hyv=#FhmBhu+Qs+yfVq1O)2Crmihaz*qNR% zRc|o^9P7YOEDS`yIlH91S72|TXsaV^jIu9dkcypOILToalA3sL!V69gUF_6#z3A6+ zM%8kIyrW6?su4|fcwoUYSfBHo1pBfJ7^dz?W5=R#bsVjD;pk+d%D}^^lb~i$cG@TTH-JK0J?ZgFK!1|I7c2%+L-ZJ8A}?^h9ncsogtyl z7ubb6#*GV_lKQS<5`yb2l!d9W{qb$Bid=EvMTSh>yMrZozU*B{*&YLPdRoElZAw3> zVu9enO)pFFjJ?Jkhu^hw<-Td99#vY*H#(WBZ;B|psu;u?cdzExy|9sCW(FR1hKrKXayjKD4wM48Xh{k+c;6))F>PA)J^~`aIZO#lYR0KaIRuOy%1; z_BNpbANn6NSdp!1Ts`z)H1)`W+G? zPStT^@e)YoHkV9kgBFoPszm(@cUV|Q>ecSDQ_w!&)XA$f*)gN9Nxt8lBh*T}g_K7j z@W6XZt|^QYk#X|}rZjsqoJai+{5_zxUB)H@nFyMJM!d+PnW+|V76&8`kJy!Sn0fEMEY0u<(oFs=mx+D@TC|5aR2=0 z%;7;MYw~xA&QVsjc>WgM$6~KIFDRxQSRqjmp`}_*F0owaKBa|hHBJHysFf=R!LVam zl5&xxd+|HTtp6+p8npztjjf`thEi6GVw0hBZC%9crA=eDg z2hP#1jVl3_53NoSLS+HjcrU!Epj|6K1JN0>$@gn{nmOS7Q`h=?MOP$y+*-S4+1acr zqYFty)QEvSwiK#YfWNgTh_NNkSjl(D6XfvqezxmaIT0gafV2Yvoq(0{+qzU0iFZ9! zKI#;~NiYRytWGb7%7oWT;^xAxKutpigEhC{_uD-T*vT#5!MBDfFmLM(%d2d7;9`-x zjlR>c&+S68lsla<@grlaPxwz{!-=EfjD^;O__gS;{z8e6B=5972fhi;er6}=^(84G0nL8(~8+K&6gar5Vj2p3?V8PzrNjrM{Q zc$lOMM#~L>VIm-``+O(y`G=Hfe&>9UN&1f>|s=ugjZP-rB1lpBuDJ75!Vk|r93&oB1<#sK=2@+ zv4m6m8HwYM8rl&W=3MI(vz&RbJu$O)YD`ni+wcaN#TN$WdFiw{40hBBp@W4LrfJ}q z{70nsA`GV~pJ3xIt}R;CNmIzLo12m!cj$`$YS1?le|Q*~NaQaO<<=l!uhvCIvSSM>6&>n~c+X6~+|@s(b*T!6*H7F`>#Fb2$@=tu~{}%w>-vI?Ivxs5b&SN}#f)1REx^ zZIt4Agk|+BBj1?bCjy}y9bm0U$TkmCz`_BurSdsJ&RfQ^8c0BNBOnf3Lf15h&x@H< z^)uO3bY;$zJ8~Up<#BCC6j>R(i0wMT-VV%y;YUvY7Ly7%A5%S9j=sJ!Sb|7mm64~( z%aYvsgRk>0OHaK&O&z$XnRX$Tw6|q{K-O`m7W9RcSBC+6AI&HXPyEwSo;vr~HH_Ha zBzyDv{)nRAnt*KVz*yDMK>uX+=6mBFjuNldPF;VJ{LXv+`1Av-Zq6(i~Zs3iT* zpNrF9#_Znt`d4e;d4K&RAV#)z+ZVXN!DsbgG}>7vWr`QOr1~vJPHwvw#(x@n{QY6e zkSNJG)LbKN<)%x{TMAzD&9|7#sXcI3;nC2RRg{0ghgbd19z^$)u>6H>9xv#+M?w93 zK?7OW84{@{B21U4q|HYgz6clR7elhDzoBYNx!u4mkeSBOXo(Ag0VO)P`}6mkslquu z;^&A!VMH%B>| DQ#pw@w%>w?h^QASNdH(Fa-c%?ndpm$Rb)EKr;Hq=vwoMYyNQX!oEWfS@dNAxtudi~*6HzB%x5GS7O-fI*%HlP zaAHP6EDV<00=q#+W6IZHHYZRlI4JX%ugR$)33mCT81u*Lnu8a;41snRQ_bGzs7!-V z#bqvdTGLA@Q!4>PtfR-m7iCIThjNj?q0o(ONEzXtc3KfC9zGgEwlf z%Y9$nT6_B@_XE>9`j$R#ufwcVKm7sF$nDWXDx$TLD)3aRF~m!QYUudmwkI%Vl#A|a zOk(p(_3@g%;x%2+K^k|t`Vd;jiCVCVoikUq^{!A;N{M<`Lc{9kld0Z4z9k}bsi!?l zTlF2Vf<|lOVcf?Q=zF!Zdl%LX`b&3F5`oSA+mw^nkfSFJop%HJE~sGmn39Xf?6rNr zJ}xDHGY(&7tJrQ=uVlN0HD06oDC*~YyIbZ@%Lv~bl+hnPw%hGSFC^+Y2 z1YKclO4m#6K1y%qW}s-f`?DAZ(8d>eRL~P}KIXMK+VvDG!?B1F|iMm0o}|cgG!~yd}cduSt2Q$SA`{!nWA*K@|aiMfLSNoTe0Ied4%r z()eR~M?x;0Z?zMK0+9*wP`1^4rr&@Fj9JyTxvxYs6%FsgeW`QUN9~D~$zfZ6V zjs@QP6xCSj4rdSLRP!Tqn85Wn)^AKYj6h(nA#E=oH^u?sdtye1$T}xqNfo>8f93&w zN)l$dBHZK*Db55Anb>y#O-y&pkRwWf1;2L8~ZmLLUSD&x?bp2$6fudx3Qjuf^JT} zeuA46JOLiv60qG2NX{!;$*04j3z!5QzF`@{vOps(KjvV78QHN0)Wp)+ zX@qCHM~<;}SHhNaCqbTXm9X=J?KGKSQBK33Y%e=RSn2xG*#bEtK>Lb3d=Ll0?LbZ` z5X%HXq#U5+U5%~U7uA=jf(!VEt=vB)G3k`A$GIWi4Rt^4aK~M1>_<(V-a*@#r;Ea| z4I?iXWg$$wW0$CQ7gD|B!AV%`pTD&Cq0kvgZ681E9aYx7QsKr-c7hBw1N8Hu4KG(@@ zZ)7Laj!yTjgCR#k-I1(0&NA~77(DFLX!AcpUL@s4w5)i_1?JMUF&x8%*>nKzr|_yd zG5WlG1*>f9#}eP!ry|u41|>BBY9-@mo2v;OUJTw&><4Q#MyXbtlnjgzhk-C{!WVFb zjX{>i1+Nq?n}&<;{|vg+qlO%_jdG`Mi__!{-e6|Ef>f^c!e#^*sG6;%+UUGrTugO; z9T$tFrD?90U;C!fi^WO6D9Uq<)B3i$0?I5g_mqUXsdo-s_COhBGuBG`t>)WIne>yd zqNX%dZPivS^7Ufuno5}L?mG4Ur0G_^u7)m%m1G3H8VN72j`^5&=23dUo?j$lquW$HC*wA;d>7@rL06$fGzI?A|^!4U|#dDUt-hId1Z!5dL4+v|C3qRy|GgMzl zUw}QUmZIcyYq-PJA2Ak!s#(iNd+*AWem;GIYRVdO(gd4~KKF@t&Lkh_b$$75{eall zUkH1Enl>re7!&aSRB6q5{;D}aCRDpP4KkF!s`};B<)`B|#Z5ydKJe8(^?Tb-vNl&H zz{(sUXo)IqN61^81TQDvs->`->ZRb=o9WnsGp$)W7M-HGbbp^BcU`Z}>6ktdv`7ic zcGh&YI_`u>-1A!A)%zZW1-_{(m0kirTicsFu2tdqahmlQ`6$1Bm9sH=I|(y{pnuHf z9;As^yB4O|`LAU57K_f)z5@z}Uo}6(K8N0&)QcW(d@3#b@pEk9ys%rkLHsWv5oSu~vsI{N6qAO+@$*U>?K%1Nu)o1(LrhoYP%9U72O% z|LI`vS)o8WEWY~tXK+Mz>P?F(l@IEM!E7A@5`X0GJ=>gcXNggz z7fF$rMjLxdC7a`LPfg`Hh?pkf-QkbgW(x^I7lH)Y(moM01-<^SQhHs`+NL?_Hp70; zsSiF%hpYq=w=2QS7oCJ$&-WtN2)unBg9}T37I&9l99+46d!+myV)~GuS;irA?v$xy z%Iqo}CZ$#;_>)+uH@r)1(peQo>k$uPrFw zY%r@J(baKA8U-o>b-XspPOjQJFZmLbR^!Y$k2S?kBOw#0GPQctCGsBjb1$3hi6D&g zL>uUv0@8=iJOOQ;bE447a3x6m82!R^YV_2E3CQu{Jn!_NR2^{nBK<=~EfCgl0Zj>Px-Di>&`+MlNF(nsi$jlmDzCWd+cwADk8rhyn!?duj!UlEyQ%v3p2W_)qD zrgce90daKws8&TriuzBPyvnbVad`Fh^?Jl8-u~P8XnYTxCLUs;*U@9x{2*KdYWpg@Is4kZu2*FJjp`cW$G5tWnx*g|xY0nZ#T*;^(YssL{^`}72X$Yj3U8Mki; zzT_^Tek$W{#d5)&=KyzJ#Jn#5g|-l%mu`Gav5bRa6y7DN!FOxuBZilLNuVdA8wPhv~T9@^_k zj&z@vrQJrUtVdYm#P1XBp4r}}r`>TB8(J%3OYg(M?dxI;0RgY&l-#GNSThq=l5XEzbWXAZq6`}A|pOSBj~`DKGDg1Rub9dmm>?Qh*q zQaE9#t~c!q1u{h!aA!3?r?P!3%I7aE>{Zi}ZS=LpKmW}e!C5OsN5Lpmh<+*9p2z*3 zoVigj!WUc~JQ=}irF54Z_TP&3>l2wV(3Eyr7P;U>h7+eOA?x;>^SxmR0w~=vslt}) zvF-QSXK)d4<@2xy9{7AeHPdivISU*O4{|kxZAdvv(^~W@kFTxm8ns?$?%8g(#-;<0DDJU{ zR0)byX6NPv)Xq!RX?Nfbr}z?_3*WeCu)QE{Y(1FsoNgw@#4%d_KD&u|_De@H(Ds}Y zgQkI>pU2L#PYeFD-wIn7H)H-_`4`kHK=d6^UigMVreDb^PTooI?i?nWNyoOP7>Y;G51Rj@N&%Q2GQ7cA=PQvWXJ zw0_8@=IJ&2y~Kj138>|3KNxmg)bgEXx)*z6G~?A~o0WplXTwoDN4nAQWGT zY5opq(|$N$@-w%2>ZXn?xi91U%eQh>J0W-G>jqZ8g(Ti;PGYWJfrXV;bi`p zYDOxkXn1Q?df30hEsceA#inI{n<`;Q9KgyJ_)nV^Wc0ES#-kKFA7(_^nVE4B@dSG> zR!5^9&ozpkXA2%0JI~tJ17fs(qtihf2p-sgt6Y1|kpZMKDqv{S#*R$0x-%fl7rB3A zT>{}BrI{yWEksG4<%(HAA1M(my=sc-y*bfbnfCLgjQQ6oCpP-w$ubr;JkHC0Ik~@s zw?7$6;eWA7W}SD`YN_`Axjsw4(%8H%P`hgT;kdGxl4KT2Shxo&LQkEk#ph2$7~ zEyk{z;7x8eLaAI%1}9egHyUROba2+RwMrgN3Wcg5K?W79-El%H;5~UV<{>xN^DEQG zxK*`C&K)L?rq1Dz%v1jE{rE^hq~--5(5^fshg7w(>mLoyhGwp2n=&Zw^bUQK_Gwr; zm)QOHAiH0437~uwBn2(|b}!`F`X6yeE>Ayb3dit9dP$Rl;c;)rbue5 z`g#a{2u*2dkJRsiZ!fex4S^h;ZmiT~IV_@9W0J-I`8;Pv#ct8&ynwGewBut06DnpB z=$2gi3j1wMjH`>?vN6~5(?c99L8&vHLji`%t+%>Wqu>Y|Mk&58sR-} ze;_$nfWd4GYaqB*RR$Yta$={P3U1SNtpq`JslyS6Pb(B=&goM6!qvmPkm*48teblyZ6EYDpJJ~d3nRO00X9s_p1nqFlRmJtNoG6^K-3FGP(!mMH#m-Gls9#j0ga zTb!5FG{2Bo@ODn6fR)i%U_Pe@0Rat>l^ zp8m-Rg+uEbJw=Y)y*tD93+b+vcWFwvQ#QUxo0W}b2MDMQ#OJn3V++a>Yg|{-DB=4as z@n=NTJDDf94fYbG$0Fz}J@L_DT`NI)qNef3nBS27hs9*EdC%71tfsO*LE8(DjGSQ*KxeST(_^^O*-(#v z^0JWeiIyLFV^FcLkDE(;#4i(@h8w>rYl!DyNm4?Pi3YrN9Y5B|dl5QFR8K^`!u7Jnjl(h$kaejeZBoe|apk zhD1z_qJ9hyUmFQOEi_U*{f*mBdoMNKe?ZX94GbN?AAS9nF5j2%1`FhESM^Sssc_rk zc6RTV;?F)WtT!O`wWA*A)fqkm8n$+bPBT?1+J|RRezb%HDODcAFLk4B{w{&bVUkIR zXlrD?%slpfEa5mRn+^#oIM&$*R-ubp91AJw<|QTzA{?r$--`F8?+o;XlKGt#NE3ae z3^@Fq7^h5cdy#v)80rDi8q*Y%Usfi$jlej=)Gg=Vx7j4oCeSddT(8YEWelC6o%KhC z4MY|nn*F@kJ^4*GcVmEm5Tu8>#U3J8ccm6*d{n3hg!ZSYTD?T={uDI{0xBxBt2t4R z67K_om)v{{vNmA&N>RCBWB>Lad}u4Du>2t6U*fcdSG3IiW>c6-M3#2G{3kn!$~D{M)&)#sCe_v3J%@A&VfKM}Wo zgDkLnj4eF*yZx24je*xj)sll~_K5X4(#>|?Os}c2hJ_zVrg)X+$liM;YLxgf3x!j=ZhZ`Bk z&(mve^{QYI+UIWIiBmT@v)EIPa!4U9;dvExJ<*+%b6FKHj?nlMxK6gDe3tx7C*45qSBcahnBeIGy}@$aP$ zJHC5-yF@qHQq5Ey*zv+3ce|c(<(+xas$Zw0A;g(X!}F~G*e*mM*%}|pyD%8zoi_6V zH&imk!rWLL3kR&$qvz)mvrLhz+jW-&FKs$jDspTX7jk0ptp%(C+JzhHTl8e2w$1EQ! z#{+$2W_UmnO#cIs>XwP3WdfOpRM~vR(0dkpR%>hboFw12a|m=QotXS$KXGTaQ4sx1 z!R~#W1POOT&M)doB3atrWCv*%b^2W?6*{M-4ag&DXQ!a06Xmcw^p&|z4H4UHZuKc^ z2oFv5C(3K|RD9D>Iie{Wx`Aj4a`%-TsN`8B~IBc~0x>2I1ZzV)(!B%_l{2LHlJD`uCYB5}&$_;>HYjA6930o6;rk z#n!%4RH^H%Nr50=>{EUcdMk48!cpFEOuw{*WtB>DXY~nIvNyqG&RhnKN|T4+*OORDe=7O){MWw z<;2DM!P|2`rUA$3IjRv+0(Q70{)h+<%7_a>x^dEqq-}Q-tWs6ZTcuX#v^rLzTn0~P&yzD<)uLYDL`s0v+&NdrG&mmW`L=G@M*Z}psr zZiHB}mIC;Gz9saaHt@rpQfGo}7CjAgr03JAYoNq9CDv;S(wJOxSolcq;HPTXcY}Uu zI!mu%{W_k!iO*}B@u1QrtKh$%7R3~~)2k?WC@nd1I$i^-v;`Q{P&*D-_~qZ&^#OYC zD>g#^+zK5>U(yZn!5oLZ{ceCifs*h{lL*3+?XgGdeJWj`IRC*hML*a@?sm60Zg5Bz z%N$wCX92Fd8!8ADjViV^Pms#8puu_<&03u|$3geBsI{(415vC11B$4}w^M)cW-4?^u$K<4`q)h|(LH+?SO2YxeJd0I1-fGW#w zz2q3gZZGbgNmjBcplv4MUvO)$O56K|spBJ$PY4Oqb7|>cWuK`+=6%0@Bx`z09m?;; zKihub5;dbhv*Fn?tR16U!_P#iQ|#jlcI}oFI_}1J3;DH)x7wAdBuDi#^pXaK z4};#~fZ^mL418A2^q{H^w=L6hRJzX<*^IvNr-;e9JtN%ID+uyUVU}!2D>;JX1nMb= zV;%;MG!f+0E|qamv2+!d3l^+<6cPFd{zCdZJk}Mkt=GS?XKn z=~%lvmANHB%fU10`4(uch%n(HM{%ETT?D#p16W-AJhqwRo2-5>c80F$S7lI zK||zo`Uvw-5~T#J>&Ot%eM#4t2{oIvw}Dfq0d4xPfsnBdc7 zcS?_Wr9^;lcivfzkw~e*ASXtainm{w6)`L8^`PD0(%H}orW+0H8$#Of&tqZ(uCLBb zEjL}W+R)j~u(8bqp7pu{5DNpc_`v@V4XG(YeV9uK+kffLKWG9jyL*ppOOglx{Zz_;$e2T0_+7ra`-HS;?Ate1VE=qBIt)MchK{w%|2|x^(v)k()M7}F=g=ejYJJxth zvf~T-C0GT;RtSk>Bt2ROG6(wR?OnbJ5S+=1CEwD|nuSfN0Yy7W0OsPq!tUFd_R8zU zAA$5^_5w?^bW2p!gJw!pHRV+1qfD^|Fset6o&^ZZjg!DOS^B}!l@oE@bE_C~k64g8 zZVFa=TM?ymKA!>cxmna1qbJ-uN`mOSe=hn5R_w=#R9V|sjn$u%-RhXdmG{31-eh2p zT*jIr*}6c0yAAEEw-S2I@6pr$yv>x`;N5sQNk8^$6*3fd`rV46eV5|6PRjHw3qHm` zQbK1&RUf~9uO(l1Y$%l8ldn-|KEF2+g1+A-C!@N@TvBbPkU+bY%ro?tR7LdRYHk8q zdN<0nQIY*?0#n^30EY1mZiTfqAGm+8B@ytauE05mJZ%#Bn3bw(ID;rj%9?r z8S5}!o=Op9)FDKVClH4P#ymOu|7J6&*PxxfFg85Re3_(1BY zSU5E^m?4lAdHoLyBSU{PZvM*{n?37eogCfXU!;!`biPkL$EdHGx$S)9w>7U{LqJ0=mMUUNFXi|q=!KDpHBS4 z3Z>$f7(p_ld8*S9L|gv!0&-|)_Ux5;qlEEntbmdd-5My@(FeQ?E)HK> zuKd~6>|{lb8pVwVzp)}3CfZd49Vgf$zodR&H#m72n->&ojJKnoZ6+Jqwzt3jc5|iT zv546RHmzE6E-?okEwa1cA*_|3V|7TrpTjju?@H;9c5{kW0_V6eSf};?NTLGi*9Xns zsenfv|K+V`lHEUJ8rA6VXbMyNgpe-oC-e;BKUuQ|Jh_uVA*iB~sm&XVp*OP8A{XcpsOmSX{#GDUS)e@6wgA_yGr3@V0mcqLs zf}AA3$Nit@3O6EDvKe7X62z6S+4nTDY)-X~{w9{c)9g~G!My0UgTYU-a~@@6X!9Ha z+=2_8024Hni-m_I@k=6Bv&r7QCxlcCO9E_BNMhH?s&WF91VdAGHm46@gy2$UaO5>8>WLAl*fPmw%ZEnoJug>0!HUUsOYYL&2esd!^{o1wALN3vz4$PvMjjFLXOM@B zn>csGN6-HKh?XhTjS(Z0RhzYM0)mKZX`tfU%F7I#Ibt7Yj-Vy|V~+St(}4XNY8_!3 zD9<_luQ@_C3Ec$_mHY|N<-J52FbK_F;n8YW2J&3+2+g-ojgaQ|ITk}U{Il)mRH@#W z&?puqH*EZ4jo2rQFd2XKuQg(*@jt8)vj1a^SU3~A`*XNs@t9M`s@rjU@=YQCq9bgw zO()^&-A@b{%z^XVUAmPPZiSEDS7w$4`c#8J=D~sp=KGVSzC-8Y9X*rkTgc~mD_d6+ z%fELYiw!>~+^U&)u2Wm-CXnHqM?t)L>1%klflV_%+K<0<`y~0)3;FJWQcskpjKe7A zS((d~`eR$)BHVUIWfZqcyW3rPb>BenYGL-r#f=LV3XZbmel{Bny81hg9=Pd6Q{Y)W zuANY@eaKC~aq)adXBS1-zoT}7e2W<8Z!~;fU^4#xG$7FYu_UD8g9(3b*jAc+?#cDg z5+EF&{bym4bQ;s++Q2j-os;=Aj1cMKtSzpN-&(TF!u<8?w@<+_joj29NPXv&oVqoO zWJxqQWM`6Ce#av&^%SKG)qTMDXZ4}NnEavh*sU_heQ5Az-J2}<@dr7b^~7co%rd7`mrVJ)!bwlcL(6{HJ^s=FQ|4T?&6bvme#UMk6Xd!#3?k z>2xKec`WBPT5D@@+A&p&44YG!%+*?_(eD&Ou$5>0jm3MJ_zcOLRhh-=o@kqJ;&$>x z*4=1^`)THL?kjIkX(HHjTKZ^Vn-uY)6Kod<{)drZa0x|xO`FV0jZc~W##6Uyx2WQj zhdr3oW69Br>uX6}mK|vegXzqa>L#UoswNel z2z)-vB4N4llTVW?tTWd-abE?q%^#%$+n}Cv=Mm1D&LfXGW-GpWkSgc3MoUxsZM6|I zttmyTc<8McuU`HM3__x1nNRL~j%_`#B#yFdQenwZ`8n?v+|Q*_gAe z4-#>&U(}38EH(9>2f8zn%XNw6vbXFJ=OL^cR9`3mfqgz9aAe9!WA2E-bs;eZHt}88 z_Xb)YE^31j`2nX!24$e0iI{PsagdKCDEeabA8hcxPpt8aDw`fb3S~W-@#W&Ex|T1g zo#_luO;*NvO;Xk z=XamJhK+5!&i%A!kmIc-dL!$EyVhz;9y5;(=RDe=hy2Z|vQ9Jn`F&^{}Nrd#I;pOdu3zWI!e0-vkBamyg|4t$~%m!JH3 z%BnS%Y#oWY&i(ueUC)y#GIt7_^JXLu;A}zB9kHFq1ny^2GGbg~hrsWH{LVVfApK8I zamZQP5;kZjYG&r+6yxkpg~`M-2zwt{j+ha(|AuKUms!ONK2d9$7L|6WB)swQ-{KMxATGJ zr;U$v1hp0sNPCebuJo1s6avRw?#8jI+vXCF`FnM-X=d{hU(N&Y{s z05Io2u7HpKx&oezX40nLDJOwa;AHaMn3s94G2#MCQ}l$?0Vu+gI9EapqJwApk3V3( zJRRUu!r14j1bgIbWhis%p{G5UNFI<&PLxQm4+IvyWD`GuB^1&=J$Pv#K)PI8(ow(N zhZ>{?{^HzypoC7i6fDLPj8i3JN17Jj1zc$DLei(U`4Y}_Zh#QGZUI64UAYfH{i8@% zf-d3qf~6?>gEI?(qnOy-$h4p&L|-28&psO$B(+l?(-A(JA0P4=(B}msI=?iucMuo$0(pg`U&rK1;{-N~I&Y?_9|Ve)Didf!UY=6l{U|2ULpx zMZfTdsyYvH;>zYRye#cLZrzhJ5d{_ zEjvS7g*t!l4L5plX^^vzp7Q%TlJ9Mh+$FS|8+Q8aHGSUk35UH5FmY2a;K%WY z<^pK*FVkR3P>tUPv6?|+7aMLzZ@_1aspOpbr#;@PyU$#$!$?C!J{S`6Z4BF%8|Wy% z{Nue|`a1kQ>`rE^1DS-`@zxDSrp7Ho9G-tOp8g-M1ZUwH`Tw2A@Nc>%ITze|YyIl* z#vsPn75p~MK}J|N$Am9p`PO7Ds`&XpPS0tuUNzuvx7oR+5I6r;Bh38|em|fmN9r!8 zraLP;vXt^GjQ>nlpwq-al_}H|Z6)*(Gql9U2RdSSh}P28`24%h?0t%?2~PT@ot{r6 zeuJvSuf#gwYe$I%EIJkd;mpq)FARk$V1Li3(EGxzAxK##^>U4*%~FE{8o=7!${e+~ znt>@2z;1LDj(gmE7=k?q{1(BpWU$*tR^HbK(_buM2dlKz6V@%_ML+j2I)fT;Ljpla zWmA&#opbJ>CeMA!B+SETfv~k#S-qe@+ko@X{TrSK|Ak=~$In{fU~@^lUbMNcfA^JS zyQ-CO|I6puUK>|`7!0}CPNUIw`UBPLnBb2WE05mI^LR$l+20g8;7Z&1v6CP&QNl^( zRJD@ej+G05(zx=mL-1E%wgK^Pmp)hEvx7JM-yz9X^x+|=vN!)i7J4{?{<>_jYI*Ert}SV;y^*&VD5IR6UeU!$Z5lZy zFxb|eF({(_PhY82+aYg9IpSHCHNDyOa@)vrkzXWAA11t>=NP67stulg6>=2PwWDCio^u8<+oKH&`2Y6DsBYfWn_j ztS{Wtc}`;1+;5F|mw+Mww`Xgdij=oFr*Gj{4D@J37d6BGMc#V`HPyF$yYybA*HBcz zLYEFfkRpgQ1qJCM9Rx%`Afcml1q4KTZz4#O7K(J~RS*KwK?ofZQr-n#*L`32yPs#? zXJ+p`vuAcb`azwnBx{}L|M(rhOgo$E*h?a5C07pm?v_6CrNc*(t{j~8!~qa@hwVWX zzTN(3kQj2YBmtNh#AHZlX$!&_nT0ik8@rrM)3f-(Rai2<=Bj=DS-iSGI_7>J2w=!H z&V1t_IdZ*yMiR>7%7{6r`j6x{F|^qQ;{?<4o${%b$KAl(nSERrge|JRWxsesk#u4X`naj6td7fQ6EwNE ztb8vU{Hh_+Y;vGpSYB)N6bDWP3`@2fS2-{QGpJNU6K0j2>=Oj_mt6vtcCN5* zS6sQFbmYrL(qlgP_$YP8fZZ%6tm4j=U(W5aCjSbSVhlfWrsCw6;sEYdnswS^>uM8< z8+}BkkV@WHrLMaH>TDcGS&VLxZ%bnu4_@j|Ma-|VV6P74kIn~{VmQ^~5I354ZdB|_ z$5XyVHQdV`L2G$vxxMC5*>4s#!S{~ndY0u`&SZWstCVDLF7meP9{{6~IT0qb)RAVe zd$QbsD(EgEBx$&5XqM9PbzEP%+T7ig0|=(WpXgASiv2IrxTmOjz@Xn<` zD&CKTM7%*~o2%gyUdeg#o0Tzm>Z_4mp@t(yCr^oQ8X_2X1SZql{rsu?(XcgEw;Cga ziK@wD5f$6CTy!S9Y2p)3!K^EB&%s=Qtlm;Uq{Jqxr?qZ}=7;7BUUeL~e&nC2S>-GL z%|p!8=G%rQIiuN9%H3GmntM$IQAIu~WeGQ^y)% zGdHqN-drW*>?nv`Q>22=*Q4ZhK%Eb`oN|<@pyf{iowBhC3V!$&<)Y@ z5#^gryYxumLyc6k?9>RBYU$=<7h3zBje%xkCrci)A{SqQ0RZe0LvnC{62c|bEq^Y4 zyvq*S6sXPxaw?}WpPcJ?buO*=Ib$NrL5o;gL7aJc(RN3LV{@;0Z=X+Iq5zK|j^e8& z=Kc%aA3RaLv$y z{P2VeZAM%A!l#@7&64j%chn+TFq_BYyJd>uaK_%1W#t6rab|n}N?0pod4rKq5z!HO z$eD8(Vo==7Q=|+}4hs@(9IS_p{SsiH1K4{zfyim} z{;u6-K~M$e>GPm{G`=lfQ$^U&V%>VzyvL0k4C(i%yFB?{pt0bQ3}St9;= zSLW!q2uI>QMxxh>#fX$yuOAaoi3(MLYg&`|Vk1|b|*1LC0=Uokq0?yEn3L51h({%bZS zJBGL+i-)zbSQzD`n>-=4Svp)N7K3TH5~cx5MhVVSm#2J~w=%C*>tzv%lrlK7y}s(J z0LEPzmL^+F0#Zh@#GR@tJ_D#lEVUf2Z@)@O`BcOUf}%%&Y~Cx9=ovLu3lc4wIm3Gg zKYYkk*R6W;%0}>RsBzhEqb6*-=O5gHPHnQY&*@+?Dio`4@ygeZ91Io5AarM!xl+YD zgkwCMjfrKH!bXPDWUkEpROsN=EQ;zYxO+mB_r0PIL4#b6fMN1D`9V<;OtZQ}OyLgo zEm)Fp;-!OIagKLdlGU9IT_HOjDK&90=a8EuHe>M}zp~?|Et2;*9)VT2M3RGo9KNqT zMOq#w*w0^VY(^4rvArh?sHD#j0J4DUA?;M=S0*T}S7F{%}W*gu89^h*Tqgy z?=|=-l)mnz{*`AinRWwdMcafu`0`Hvm_g$R6tsNI3C)+qy^$HhJM@H zFqdS2V8^*{#brASr9iU`8u#wHHV~TxJ^3*GO^5Ro%q>r6Lic-*C1msJQI{oQC30nH zh%sMk9Fh=+yivLvaY&53HjleHB%NHz{TA^OH{FUS=lpfccVkhNet!O{QoTZF3ovWK z!{(p)5pF5lW4w~C^phqit)8qP7?IqLbroe#z|rvZA)0Hrdk@x35Gyo!J>Xur`K~Zq z8PCu=K%{jQ{1Fz`Wk+@(4;=9MT0hU75c2<~ZM&VA^mX=*L+3Dizh|L{X_lQ?RIq!& zwQ752GK%idu&zI3OF#T$Jl9HJOuWZn)h~L2i!aDnCW}mR@Pkf1^J1u9_OVhyru1?Q z%}+7umhvIUkHrdMRO8D&FU+in8kgUQly9U@cTN<|-D^_Bhd*XbpZt`cI3_@!b1poG zIhb>NA{f9YTDC%tJ=9Rrr)V4E4kOEZsJ!pGU_K0C%2zcG7>*dh+^N`WxV;1Iu&PE4 zLi8;xo%?qs^&3LLHY;M6Pek}&D!XedF>JEJ`3yYj91)!=$O-z0gQ&&b*yxvMe&}+@ z3?kFQ9V-c~qjv(wl#9i_GSoS_l8-~*yY;a_`9X{668{Bf;dr>|KuiZuXp5QG`=lG7 z^%fR9jMBCm0fL40X!RG=YPTG|MNhs)^QSVJ!1%T_@&hukv{!NWd{#}YWiy)1;Lc2S z1QMewnIyd+Dlgr46=F#Jj2L0)7Zm-t2uT_o80dyr2-(VCsBRNjCtOOx;pN-tkw7J}YRyoLbj z2!Hb`;%%W#!c92+oz3$fGw6!xEzN1m6P22@;c#UwYPU1u7Jx?zm=C0Qs*#LZ(Kr7& zZ2r0m>i0qsw&)T$4A0a3@e=RwAv{)-TftxBOFqpy=1FX|Wkt9@Xy`XrpV~bm8olG{ zg+|UU&A*w7heEA-GVDaax9%)PaCM%OS|Gw*Mk0Fi{qD^=v*ZIzHuusO2tlVrBe~8ux)KjwuhkN*~r;QdS9!ZEQ zee#NEdCLd7;A5i7EwYb7HZ@S-6tqp#DFYI}iyKBA(1u7ABD7hVky&|pU`Ew&<1Q9o z?|8H3#MAMz_5;YeJkzHMseS4EFAR_ngkiP8Q-6{V7nhhhfTO(ARbpq7z5eH0HJx_d zb+$`JVCD7P9OE4&;{?;C38Y1&gv*P`u9=!|VO>g~>Q7<3cnk}O07>SgUvqlu+grmf zLC-LwXAiL9n#F)PLkpliFY-8_i@>H95B#^M$dX8Bb)zT*HE$E%j@B)TORlZY+-seH zeHFhWsRfm|iC2rME?44cgfoDS*tkf{RVr^Tx`h?eZ*J^E8;%mi$XYZzo8pTpKsesu zL+aq9x7z)lRhrUIa|ll0mN?B~uIJc^{IwjTd`y*-ov zN&AY*e#q(MFE@qY(fQ6I_P503C^gu{?VYO**SgMDk_dz>sa~#^&&b;9+9Z-{$*JV zc_tHD95$J|nm`pkh|fVW;eW$Ntj;>Dl7@|ZUnF!Lwa1$6wq6@ct!NajTfiB2p1^;; z)D|<@O?*!eQf87(H?0x#=q&RUPNDUu@0_VuE&Sc^WiF+}_0Ic2q0eU)AF#3MQvZ%= z(`)=)>~o*hP3uF+w|FE~n`#+|Hpn`r?_6rI=d{2gZ6c9z?U_{Egsp~3E?vg%Jmj`? z+f@s6!rQ&qVt}9_Yq;0{VD(Fo&onS4Jy2DWcqh7fw8X32O|?tV*rX1muh4w|gZsPudc3KNGXH#I-EQET!sptK+sy2Z5C++KOb+3bUx$qqhfu zo}>j!z*;D&KUEU5!NIx0rb`R37!mD>Es81oM=cjR%S3kv?J_j)p2Vug`EjrB@4vMV zPXH4MV7E;4ty1t+Ay2sek+V7C+l-orC#3*rg{?thx%O~;M2&o-&VuBR&QcV?o_nFN zVBz?aAR1B9a~7uELei1bpbj6v33qar^EsGAdr_Iq`Ta$0_vjGUychXb-N&lwTOXyn zSZRfLX$iX(<}Chf``8^n$})Uvg21!qcKXhKcjbyP{5u)bF)3R`bVR4fqD7yy z!%D*M{3bV|wTdO**5Ne`t=Q?FQ?-o)hQHr8W8j>D3x0Z=WMbevP-AA3+wYaOIUPLH zA^t#%Q+>Ns%T!GH_$Xk+8C3H zdr?WWBsjE}&H6z|dyKqe1rTm(x8FG5^PInE;zs03m2$If1(jonr4)8x#?3*AgmmYw z$rQ(^#v7==T$7Qek9q#XHEI5rYchny3YGGFiZ;8_7vJS2Yx&d5FUJR%*lT2-IhFiP zMSPn_L3C_NHtfui;2JOeJp=Qd2a1;^nG3f{M($MH0((M_decQ*|E-za9<~dP`s(Vdni*vac4>LGn{1wVObIwXi;;{wwc#gi`a)+xRS%I5)SR-2mRT?hi<+N(IjuqSNnQM z(N$9Qs-NrL-ooQQnEXrm;ON(TfD!WcmvbYe<#g@K*aeIr?eqcb_3o3f_2loFgUJ4( zSc?EiXbUJuB%h!SH+G5#|7a12-qw21UuhDKs@#hj5ikOYG5#IIr2DD9b$t-OkmcX5 ziUGhIxrFd1?<}8j687=VTiakW61+zkoM>V&jP0NZX0krQkJ0S-A`h#8J*3v(Il(YB zwPAw{D&d*!yVjrbY4jGWXOdMwp%oCuJUY2(BNSK5l&sOVs=ewz*@)f4!D3r~nikyS z|1%)5BJ}K!tHK9AH%YZIrs`>Kk~Eo~Lm7I$P-B|>E*b(os@Wvh{+Y{FkPJ z?!VDgEawNiPp#z_PEAO&3@8kThq#_8Ttn{S`fnb;&^VLs<=|?-$X&`_=#b6$VvD={ zpw=&sxl&;iI8@D9Asmb&UzqTVFHSxKcC4@PQL!ZS<9C0>iQ=%6%g~Jg0yzr6Dg|tn zM)tiD@#+6CD+EBE`xGM+VE5jnL`}TB^gkd8i~l>2MB@JxlF-T4ErMLKSMXQAE*Qzm zEYNp?nW%lGksRqGEJyEVKWEcQVtwQt(o&c8lzeX3Z-J&_#e1Bwkx?ee9Mj@*9J=nt zKJ;F_)Y*hg(;?k9WX*Xj=Y%e1vO%D%TUI5YO(~H6>S21e4qdm8ymFe7`W%a=Ox$9? z8}-tH%wpxbp~lS}(E$K&Iij2pUu6t)R!y5<#PMU6bV@frK2^(<9yn}#Va8E;el$A| z9L;JdzFVn-ubtL0V%LM9#h@T>R9`MSp%3_wb7X=R5dwW6)Jx}<`mTyQ(U2CDYHlWJ zj4_l++y#;p<8er`7LUG2cCS&?jcBnv3N)x5D@gyH7Vwx%4PR)`VID%SYZONz8_3vt zz+W@J96u3G(>c0mwM%XAYqtrIxLK3Ut%X%k=ByCX)Y8wUC~SjN$6Y7v z4im4AcALU|u9C6Gm00Ovg(LZds?5AT6G0P5gK+Av{69S*BGzI?n;y)Qi9Dc|EtfP( z(l1}1YEJvj#@;eA!x;C7h4B=2ZOFhhvV|>BOlYv-airuFNZsf2|9f7Fyw?m%vV<3g zffWGTi5ZBSJ{eoq9ZiLottpmt7=jvzY2WtwZfR$8?!an;J zBV{wgcM7h=C2OIf)M4Jh=Jp5T(`V!)spiWTgC3_3K-~J&$(K5WsQW%oWA$I@qH^Q< z&n^WS@>WAwR$eDwL!Q;a;a~7YtQ_i1;}&21?O~KOAt@=BoEe&mj%So%Ivlm{#2tLK zVx20lg8zKaU$bsNp!h;U(RUTmoI9hWPMaYany^zWxF$j%f_A)B+UHJjVOw-{4kWmb z*nCp7{Y{6dux(*U`+zMt3W8TctL~I2<#3LR6a%;jt;JcfLKyhff(7m)Rg%E4ND`P~ zRixu1s1i}wn|V3+)@a-xOypmu{@wOTo$So$QDsK8#U`^?WKn64e?+~>_Uzm)3+|kw z^zsDe>PsqqvdgrIa+sUiK}&#OkwOm;EHcqG4D(wG4*n+)@hT^d+zh;u!yT;Qd&Ew< zUx%Hd5qUAvUHIf{#+3~BTDW4y-RBUv!K;<44-ci*A*4wUb{SMuYRhs zYYlXhKi&S>-iKt^``DlJl9W3rk8pNXft$I_9&9YFCzw6(%`0cNhEfkpmbOX};;g6Y z&yhE$!K*B1qtaaB=vme{=r;QBA{*g#LDJOVFyCnl3z>2yiUuB5WF)jG?z(+n_ed7l z)#ZaWY%rvO_7H_VnShUs=Iy`8x{8sRKs6?oB@WT*-5ffqIyI4qvmBoP5{X#v+;0Ch zq|OCjjjHo<)XjQQZGDZ7uqktaeC)c_PD+}UH)3MmMI)W7cI6$j4=iul-A~^Ib!Y+) zfy5Z;qc#W_@^k_57_rOvVJAA|Q7w<1XLh;s>jPUYFI(Z|yT6C*^S>1B2Yg4o-WD^Q zPl?RAKaz*!DRUm&e@0wz$Fbc{RVW?xjgQU#12iV;<)PYbA`ldCA({rklq?7)|33{u zP+v3osau*sFKMLtHV}18d=-j#l87MrxpZb6BjIS{la}MVdg)wq*vqj1Xbv5hqtlU% z_!NqrDC3FQH@$?xbwXQiFFs93^V>#Krztg0dbbyz>cQ^xs>$sDP`b||IUDk0l@&qP z`_bK=lee|AaD8{lXr(zRyv(%CVVA;FYH{`(6%5x#TV`#0B}PL=!dmbPW)}~&?cT={?c1~yn@4bb_msoE97c~YKdZ9m0*T5KQzxP?)sokZygQSpq{k+7$cbi$;+2W>`X4Zu# zf`2>iJ;0nRm{4hBzdM@+{{H%K*6k-Sg;6E0O=vAj5ghhIUldPE$Z(5}A6*Q3HarSN zBXaj0=srE<7^{%!)4(5GBMbka@i^_Bf4o{tcm9cEpQb8ZDV$P$1V|>>tmsnvd`%cU zN_o5YQ;Z5Z_D|H|V@+=a1S#^~aiiEWWebvUk{XQ-w;sb=?m35jq#}G9W zFP1Wzwp@N_vG$br6YeVemn}!c&pwF&d!K^a$Ns0TiDPOm?-inXE@y7~RunglLO-j#(EjdLm<;h3(k<9>1%ul%TUJU1Jb z31wCu$F)E!Zd*0B9db1`FN=q^ED}r+6AvEBbu>OVPKz0A&_K=c-l(3lIRe&YBDm%m z65CCXl%<{VI4btW{Hdbq+xx#P$^opQ5yPDrE+*PEthf0Oydo>~GT>aN*x#;nEEm>m z)QXnkeEx_hpKN_D7*coAxw{q2JB$Vcz-e^vGt(Nnh@u6HqS^^Cw^ImMQqc`of;8tN zexZnN5u=?R#_L!(S4alGRkyL-*D!Rk$WG4Bz2CZ!(N=rQc)ZcU2>t5_qq%|m+)9$@ z68Po`o{4a)k7_i=CY&#IZV51{+1k2%alrwpW_B<~UA&ucQ5 z=~y?<1sG2|%C5o`_p|>4p@=8zJNfJ{xnlhnJptyebFuW#h#8UNYnp{U^fvBxG+jPx zVogb|;?>xvrD}EKcyykR5W8X&z~2#(;%|+l@+cI?;f(#_IR0Cn!aj`tZ<@m60TnW} z@jzrVos}AO?_MtQZu%R8=d@n>R%Qyza=+8pOVT{`BBRV;Kxe^G;Vu#)wro=ant$G5>oyXBRYRLyS8#g5Y)KM8HoW&8!u^y*o zx@7ys<7#b#ecvxN+veY&BMrjgk_;!5A{eQYB<9@&YE^m;QvtQponZm@-> zPMti`yCAko{S| zAh7q%Nv(S1j>(G&u2mg1H4X?r@rWZdC=^vAr{seIC4q&WyI|=!wFJU%-*-Z8WPoqw z-Vz6!tf|oIEl26#O(}TtT!q`9Gl_K1QW47Lo3b|!kiUO!+C=)d4zPbP&QHF64bHzm zQjmRIAkcdiZ$QTd-DF}&YRuwjI)B%n)>EMiTDd3YplL(Y#Wk3k2e`siN z814{qzZLU&2FW~2u69V~$<*RLrFDCL!brfC0Qw`i&>c|)1Q$xwHjwjMzWjyY;<7O@ z7x@Z}V9&qd3ZZ|)6_J+%W(|U!EQysVpL1Mp(pfYOX@t~c_OPU4{UmCghOY$^hxhA% zQ!K!SkQ}LsjCSJPq%{$mXGRo87q@AL1*yVmd>AgSq}k!Yo=9-BAWKP@&H61o&gY>6 zI<3JrW;%(m*X%{N?Jj_Er0BY=Nf&_=E#$0fb;NO->a z1Fllr4*&i|W=BTKI@DPBH^r`nrT;0%^s#QI89~j_Ea{wXV&-LScVX93@9dZkF)5G8 zms+qV`$H>lUi97wFU7q<-6dM8e_IYG44-ZAy~?dP{$TPgSuqfd5py3C=F7acEj@zI ztt&H6mke4(2z%z9-KLyp(2<*SnrZy@iD_xMz} zuEg#^S52V}k9~fbkaSvmPNY@nRgvD4A*sbPkBn$+EqrAewvS1&>m_s%@*YHy(Ujk4 z&c8BHGBjZ&+;W_+SJYZCoY_@wpo?d-;S$vg%HI3J)Wm-2l%H(Ts|jz5kqS&M_HwfD zWy5-0itP~XczJlfhTcaghOesqgP(Ze(97v~;XyVE5&M5SE#5JeB6gxk+pvUu``3wJ z{!d^1%&fJQ_@>lfuu?(H3FGruaDjX#y|kHAxkSg zTniQPx$z6r9mC+?vZ^1`b8;DJKN&S>M~OPKWZd7bAJ+I?0oW{dE$lHX7d8vm3b=U+ z=JE&cb1mu3eGZ)Kz1%0_BI1|Wavg5ktRzBb7#x!H&wd~M_yJV}m)N}h2|hP;4Am@+ zaJ>TUemc$zrsC%!Kt#ohI#5|*o8lMfb>elmJ%5JE1G$}yGfmxj9`}fPWBLS;oWEQa zF0{)|@Zyk5_8boQiq{xTG8SLDGrkx`R9+y|bf@((Wni=5WwvrAwcOZsm8CKeaKpu+ zq-hoU-Z&MC{kepJWOF2{NlN}ZbY1TKzr8oi%)YtTzBHq}e2^&+J(#O}-+5n+8TXhHpb$GGXrqZ$lm~PhSC}(IJdUG z`3~nL(s2F@9>auPYjT?WJ2sX3XFJ2vLFgL;P$?);Ly7<+(tnl)Jn9!IIx^!Ji>{^o zt*#imxy$lBX&K2&GNH2>(?){c)?I)ec$2DZOWQtkm*VQUpD)tGvWJ!KTz}5GMUQkn z8M+D5@mU8D45gX>4Z%PICuXI#BQ-`Hd2z@9q^TS0`ey^lkD^b$dZ_x?&-?Th1stP( z`%CG7H>e*XL|m@Eh~cO|Uu2cJag}9(k%@V_^sf>?gK(84iHrE!o`P8Y$R?xGjq%^< zRpQ=hf;FrrHbN_?Dqbl+j~tOt z9@z@^(V%%xHM9A}g^`=15bE>fG5%+V9MuwgLw-q!&ArSSWNGh-V*mp@wH0*R9tAUo zE`?py)TUCD5ca*cked0-b)*Xc#aF!JuDg54%pLt})%KWg*QNe@;T?;3u6zPPEvA?_ zKdcm$?t^;BV}Z!m{*muV7ltU!GC>a?Q51x;x-<{aVvWGtC;cZxSX!P*2)n5b=TSkz zjAiwcT1l5?tS?ZV4v3Bqk9@}g>oLLNRDj)|gq%Dbd(&~PrkxXZZc4Uig9 z!{uap5rqa#PvKGv94YS?cU88#^apNlXHED_uQehsK#ij7Rj5d8pI_vzf_59jDKZWYt(DOBADMyi zbP_UMI(Vt!7GUsz6F%NoFm&BW+3#`R-s~6Qne}vB=`hReIgxSs%t$M==hXdC*6L85 zBV_5d2t|VDv_AHE)0aCBi?fF%@<<0K;@(?t8&DiwdA9mw%HhFqeC#WB=W$z7_N>!6 zj(gF&yxo<+v#Vfd@&0wPj-F5N-xO{Fu6b!$bZU!QtInH} zVA((Vigq&e$QXL}NQ4+4$-RQ3_@dM=9Pw}j9g1|Wo!#_ao&uIj&@UD&b*5= zsAW6i$5fnDL}&tE_Vaoa62x+%7^RvPN4i1Z`yn-s>u|XJn@X@-V=@*@i=9NW$hb8fP}(j ztNDZYJbekc1&BrayBRYIlUyg47RrwOw3k*I-V?sAyMh1P3`#kHGTKK@pOQnX_@eRV zyOcPDT!PW`L}MGH`{)~^)R{6nxSP7a)mN`e(I+F`ma{K06DavA26 zmpb7E#=%|r?&ZTk_czsKy(iqu{1@y7&0p+BJ)=Mp&VR|KKzh-xl!Nznyx0wZ-6;Gw zyD>`iT~W*C3cO2uwARZ{yvW=BPJ7baVh#xF@tB@qB1{ha zVqWnV^YM8ecOFi%jo3T#~Nk3=kYl=Yc2Qx!tPU^Ha*FzjH%dvS;O_4j+jrrFt}72KH(lF{IKa zAQsWv$V$gB6-&@9>uu;J2G~cjGX9g;U@vJ=S%q3z?mTQt@&^1D<#K3AF^bJfbHMBK^Cxn-a(Ch;8hq_LZlK{M0qOPeiSCzwiDOz`H+?_`&oR z@Q*KT@c&P`Ot%=iPP-}_N`KBvqj$mg14i}zD0e(S%-$6m^R)t`4NM0nE*geiPY=WH z!J`GjAuCQ@&*L}FHST*ab^*h*^!1V&v33rGItHtOm+pkvd69O|%yf4*f^?9DG`y7q zC_4QIpn>$L!dwFY4f($TjS${3Z{_bb6Ln)Ej>xa02>Rm+3*-ke&Znqzd4&SkU+RjC z8-J-QG<3Df1_a-w{iUwpRnHJ_5ah)i_v^zz^trmXh8}5{aep%Q2xA`7k8P@OoJT2= z{P!3J!wbM^L}|!gX6WH7KO(bczM>GG*b?Zm_4u5_u>Ti_;b!Ik@3PSAUwM2#DdZ01 zXU@SAIcqxND=^lroDFf@ap~BL7?XDnv;Bt`JEEDnux* z5P$irLS&8JmPSPDC3fgv{h+E%?^;}oqEQ&7Z|faz7TS7d9_vf&Zu^Qozm7cAEN*|P zYo4(zOe-Mlcj-k6W%A)LUdW#ioU!zIo1;fh8uolm79*Iml6dIG1PloGeQR*9Pc zB=p_eX?^Ma7@!dJ0?44sO{ZE#OpQO&0Uz|?{_YO>P@Qh?)$oaZKzH+A*8+AkLEhr$ zBh&QTzCd>f;HoekVa#RH%dXsW{vMdEL)pL1CGd&{@!Q3wcECp4WxGgTJQ7@7tR)*> zc-LZZY|OYi2rK#Nm(D1nRfmO>Ue*H}vs5pIM5?+g3wqlZt% zX5k>*`sbe7ujF?R3KIpuUjjn-CSgIdpar*g2fG~ox>>VG;<7lDKMr6r?8131#75WD zH*x31pT@g$p-0#A1N2cus5ngrm>agR38cR=O9sN8CF5A>~wjIqXcK5Bhpu`WpwGZMK8t` zl26MW$##n1h&!Q5=0ki!I(2$bH5U-%;g*hhYhSI&P`9N}>I8f-9gtcmg+jd4vNhy- zSuy8gQFE?Rm;E~z@W#yXAzyy+PO9UWSby%$pB=c|f*mF?7NyNS zUXrDj5_fx9s-KpWCRrn2R7dRv&-AbBN=N-B^ zU-FG&1)*<8sQmMXY|afBukKx)Jrwrym#JD%sTod*8J4~SqUh*$W8JRf=dJ!#x;2>l zjuHp^c}X{b%2SZx9!gX2)rAkE??iPQ^cp((deS>$G8G3rPn*FBcMXANzF!!QYK#~* zm0 z_e>dIbeux0U#mOKC77o3S38X26KmU_SkoZM`V5m^ZC*dF^pB9bGw|7c^)4%PUo%P^ z23ru_EZvUEUOP5L=fiYKg0lkViB&_A8m7is4uTU;Bq!xY`PG5)+<4(82jYF@KDx4^ zgMuekJRB=e$2-pEdvU76Lz`~1fV80)(>x3(+VedvgX_A0&Xa}KgM$@SBO^0+seasF zhY!yp-en$ALo8N)P@vvc9BF34A(>0?SC2JRwxfm%kNxv;dq=!dIH%9+9BCGzEgPU! z+}p!HRSFkW2y37L44^{3ap-w$c$*N(eGcq7uP@0tJTBPf(+te~tL3v`>KoqM-8-iw zH`{&8Ysm7O^)gvTg>_grfOp?2mz<^EsirQn@vEn!`&No-kyDgCqWZcry!6`S3c`B* z4S@!4(!lk&@7gIBu6&zF+oCl5qj_~{*V8$=QOlv#-I&RO-}c^Zc$oei*iJI> z!jE5NhAZiKB^K}p>s-Rbqnvhohrzr^K#xIq0NU9U`LGzx`#LB9s4kKIuj)LT2L4s& zSus@|H2N?(VwQy$X!k#2j~<7ct3t)}{xbeRtZ-HrHp z^f-v)+1W3UO@>TA^yL}eQ#@s^W%}qMWx$o8DPfX}A*7ndG9pu7tSN)`YhE8#0@ajv z50dGfE#j2`d}k_R8co{*mV>ck#=zfk9qWIh{Rlu)V$C?rXLTd^Rbdjbcw(+N?@+2H zP4*W{Vevd??>3$>t-=1sG`dwU!G3MK^PmFSo9}E`@7vyJ_)Io>(Djv7U8F0RIMd@9 zR=AJ!X5W6@vB}0~k;7+}GD&~+npFP~*Bel4^k;4xno100l%f890g{m54+oU~mabdT zyxEXQOgnciBQHqL1=Jx~t%be)6X@@H9~vppao*?Y-~7M#dG0+C@n>~{vO{%lI+qeY zxM%CSP%u(UpD{FVB>HY%bM})`@zew6ToE+*4#+MbP%vL9cS0WSOazMoT2b>c0SCI( zAn;e<_uuv-i6ijW2(al4z6@O{t=N8$Aqiz`J0{*>*n-w$P||=xzX?2Dd+Fj$3puGM z`vXJ*IOH_~Rw9T)>zgbkx6-cP>=8~Qz6?~0)*@HKTNvxUP%s02`+@DS8EvxWm3Yvj zL`4e8h$*9_xB4*Xxe{;Qyp+Ky0{HJ8$Wn`!4PQ4okgT75M|A!^x(WmDBEX)+O&p4` zSZ4+=yn1h+qxge~YPg#*U&WO`YN&pO;tuh@3O>uyL$)Y?T{-VTVGE1vWqVF_^Goz* zpY3Bj5DuemZrD9d*^{7^rscy%?RQnv{StQE&a_`{N6Z5X9>o%2*>Pjy*5*<_)+Ld4 z(L+IVbH7$~HMnkUoOGUl5=SG>9Nl;};#+~1&ywB!n5FZU&$m(2j%$s3_g1b#B(Ok) zk8~NdC;Y=)a>ic%hV(7&oB;mYdC*Mb#gp4JnS$Um=@0Gu6B`bl>jI9Rs5!E>r6%N|PiA2~ zdDDQ82_2#Sj2^PN21ZO%KC%k=k8V%y<$rd2W*t^ZGc4kOAy*8Ga(S1u6Dgo3Y*59d zCdIgCHZhQO-Ws`R|29_cM`D!uaj8&63PdzBKLEw|{+A$Ln=Z(-<+~f_o|?V~jr1{Q z{gkE$T(d-())3IcfICs%=HRl%aO7d`?lbV}^TDOEsL#FpKH0)UEA6fLE2ToPb2|th zS&hh@%up=pEGP{}j))r&L9OB)#_i#hG1oUQJPwBhe_KGYX$d#6Qz#?Q+yH5PisJ?8Bp%)^`$>mPa~HAjO$0uMT=e z>#6PY<%yL#gPEQ54JtFwGROIZm!B>lq;`?p%V$>FTEgqD_*aXfqF?TPt)iJTdIRUl zZ=1QLu2%B$s*$q%m?CR=;xQ3S2YwUiUeW4={qVH+6F4PV2W=9fbv8^@%p-c4Y6R>i z$m+lkoF;2{#{@!Q*AS$gY>G_+98Hw4dbfSSj31>3Z<2BLzpcvrvwy!h6N;^|%KN|q z>{q(!dX9zX<4v1euYSX2A9tTdNqH`ut)N3tg)T>RH4YNGUAexz2|Vg>)&RVTV;e1M zu^zmz5sR$cK~5pJro-Z1mK1q-DTXotc5-~u1D3)-49?WUAK^%xf&SJb`N^Ya6bl)A z!Bn{qAca-tE2XJYI;%jL=ZOf3#Tj5q16X71G&*in+6uyp$_Mj(!-X7Q+$3|ofw?VO zgT~+ndOkQ~ud0}tr9Ij7kka7ND>5^`Pk)O+{x=nGTGTm%cx5=;29m0HK1-7Xbd zxnlSiyQd0dqIH7vuVCUlcvKpO7zL~n|*Yx#UMFHw>Gn#j$b=RJ+sHl%8Ngz z9MA9#Akjp+%@{V?SjdWodY56V%^E@h2lUlsFhDX!P|wtAq~;s@!-Mo;IC|y6m3sL( zqo^;rUthmDSd=x0y|ep@{9wMO-V7vbF+c1o3&{ayY^AA^@&hs@F@>XJI|T()5-7mx zQ+Z1|M87{3EWu4i+XoL=sH{7`Ub*LnNVdC0x*1jfZF{Fx?nQeiFpOSh79w3WzU79V zOffU|TQlPRs`_=>b|*n@%hFsX{D+b_#U+uHFa{0R%>vcFE{CQNHtz24wpGs1A`r^7MTx-#3kL)!vZD575%YcEFGwCiDoo}^LLMoc0jhV4AG>U)KG)nJz_k(e}}ghQFbs^WIIh$7N^&0<_$95qJg3=M=b z(<0NpgU!4=A^7{*+Ui4K9B1i@Cg#C|SyEtKa8oyVglEFTK7zN=8lBb-7ek?DlJ8`i zU>wg#R{%wmcZhmFPIt3rPhKtYMcf2wUXZ8xd!W^49)&2VFf=zv)ACK}#^EbVR`h?j zKjS+9hhYtY7+vaZsDF1`3Fq6p@ggD7y3PS*Fqkx!vZEX}{4xHPNPd74D&_;&q2v4f zbM+>xp`EsO*ntZ8d2;K+DfANv>Q_VEp3C?mW|hT@uzi{4pgm|bCyW`i4?8axEu>TX zJ|=M^UWYI^nnIiomy2!XGPxeQLP%lxufPWP(OJ4rO-?N>tmNpbNc>72=MaJU_nOw8 z&al=4thA}*;I(n_?Uaso9y<&6 zF~r3FC}*NeLfd1Pv>R8AEkAeoiCvV8Zgvdpla;YgKkY-YPcROs;?u{7+M``t&% zxq6?3K)56ces)MEFV>s!^Q^Suq3$QUv33$6nmH%0H(IyXA{;v&RX-0QLp=@8_Pb3H zcwRCZyL(YGD#ZsBRS%sDK12aoOuu>R%BtzDy+|b32plP!ApyiS#v9M$8vgI3wO)LU zePO8-yXQs#?Ox|_0NRchk7)-%&eaeDL*Xl_?F6^6ZjbGjlVg?LnesG80-A?0IL zGJ$K$od!OJ4bTh2J32 z7)|l9rA%LQ*4{2#d6uT-qqrOQPmE_b=k71?aqWdqdO!W~x9yV7L3-y?L$fqGcBVap zpwJ)>URO@@nG2QH^<{-XIE|UWGMeri3Xv;P!|y*S90z5+$Z)s=8IE`F z>A5qgK%m2!mOgacm_@($n6g`@_rzRw4SlgyZ}RQ#!q7Ar=@P6emcsxPZKfFhTI#5G zzR0Uid1&ge&iD4XpGp6=W(#nqr($r;efV-_23(&lMwCZ3 z@-+7$(K7O_iJM1{qs=QFDmrp-sGHtv_G^Y}f;(X-pscjGwFqv}4nH}sDW!T&He8N! z_A{~Avo<+k&kOA8aQ}3#>kT8h{%()_=#4+=4@P228X*0lL~)+}XkIr;V)87k^JiXE z3P~XPBGQS-r~fYBWhSbOD^bvP5kft~^O-D7Gz?(P4eE$rO{u%4FtWr+PuO<+GG;4% zhv!>@e|?U5Q=9uUY?s-dn#9M^D4#RKQ-K$&a;dl{ui5Kuao1t}r(LJg=G@tCIruw; zy7icEnS^^9(HA?gRBYzM2#KL@x#M#)4+tpYxFnHmNlp92NAIlw$`^Bwh z2FGt~vBP$*N3aS(W8JGmr3{cXNwIQ0#lFCk55dLQpG7ArLNY3Edt*1NNoGy2bHxbC zt!=1TbwW=iUj7vJab@?ClXnG6^eUz*@J`HIl&sY^#*s@z0gjLI$W1PDQiLEIZ*W)J z_rhNp8%As|2;U4RlywV~+G2reXO!q)c>YSe^@?-PDG?=toO#NOrguZdp>C1DRP-%jBSE0?1?S-Pg$bvu&X3fb<(Fr%_yHx^{fgA${ksR z1ZqW!&d3x$mDsJHm$5qo7Ebysp3ho@_lf#_8xMI#ed&J|+W|BG`{_DT;T`j$v{}lU zWV%=Vyz=3rmO`IRip7(X{xR|+XsXZ5grA@49`SK2nZCbtX*p2WSK@zB_TKSS|Ns9# z+1V*Gj**a^m3hc2GAl%OWhII1bB;}B$|`$=LbAy?HrZQE9d>E+aQ4~9XTL@Q~cvA zmT&$&awR0uz36$}0^+OTBFRKw`l{IH99vp4Ze=Yv?Z`04=i>i#pQ={>(|ziTzk_8# z_m&K+HKaT6NucMx+37T24LFh5*{;x< zcPcnY{E3Pqy2Ru4m+xQA>BCZiKxD%N#$njA7NsXpOPvB4+?LtGm3&JLVCHSVgHI z+5)zTE4giZp)a2C}aKM}}%Goc!#UcM@SOLk-WR}rJCEY^ufMDwo7)x3}Y?$4UFlxAhC`VnQm>ecb-JSp!d`jua4bCE5rGg z%U?016=?iT^K_W4D5{A$@F-K!L0Dinvv@E|3XXfAaPY0(84bIcnlN=DJP4LD zb0O2J`+8B3ZE=3SSCX>s_Uf@k?KcBvvzjL<`g7y->2%lNPaqc}L#8k{#MfNbpQTp}!Lj;$aE3LthNkK|fJI8 zZcx|kyCJ@r^G=5V5!hvu^r@ZSSq!>K{)H5Rz^DYK|Ud-yrSEL=R*?8UVxrjkh4I{SIz@Ne2GxuQ9ooc_am|^X8m&a(n$KJ&P zzD?!#ca#T2Xsd#c?f}iK9{*`(y<2rfu$7eavfE!X%*%o!kH761`OGh1C8-CI);S>Y6VNtg@8wM$?AlrviVQrIBLeuRBDj7S1FKyeW4?n>aC^oHVF8}P5z zSA+{5(6rd_)eo*j>9q*q2f^&bqJ7`Dtu%X@sLe3yT9`4x4qb?#-?T)60!!6`6y^|H zqlj)iCFlVAU2SBjsg(BhtA_GLQhJ9&O@S8T3(WR+e;n*m-_9O2V?qtEbvYjQ%9IMj z^)VOlM*0|?wSnN3_Z^6^eOsoQ1m$6v>&H&S`EXM#?MlcL^7sl+2^u&Q=xsi5<97zC z8(xieNOdjv9F3>&g|UvkWk$!^omW#txm*QHh3MLs|01LWsbtV{(Dqc#l;PC1^b@b3>5(w)7=0S|)_&3#bfq~PK^ zY_)38`Oj?csG0}W&#f)Yy5~?qm@d83Np@JCxC1@SBXyUV*S2rnm`{0Ga(>tcUbeK} zE|W~iw*Wst$-M>W#(%$bWB=b^z=hWE&1tWJr#+(@{yYPhpMuH$@_xTM|8 zmH=+rkA6PG9WG5;bJsc{v{ZRHdkm((9>&E%lYl?d`{w+H{b12LHR&MOxo`G>e?h+| zEO_DsHEtir1G}%30Gw>MEHHR|_-CsEp^R1Qh`JEzfTyDZF~g=30f zTk4aT_P(L#>`~kZdmsIvVBpE!7N3+vcCoPEi=jaCs=838Ro;<@RSfUim%3M8{JWTn zA6WbGUs|)-J-c;EBCqydWe;(DpYpfPYV+{9!8{BZ@-G`gwCu0hakJYKIu6Oi6rjCV z^Qd^x&n6PF{&|+CDwm+&XKNQ_dOw@*HX}a!xvmg5H*GcN&Kxnia*1LCaFL^FuS96d zIAIinT@EaMyC>N1acxZZ=xtIbd@TI+3M-w4Zh~zG*B}C$`2AN>jp5%=)L0%Dl>d|- z`uGs$x;;XC0OPtK+`XJT%TEcY!SQD#Ue_mZ-%v~@o4H`-l?=9DJkA2^y5OIdCKXSs z`Fmh+d~5D$^oSV5?8R~G><5E!5M{0HyFDt}0y#@t`G@*K#n2^hp*^pwgO0T=-*bzK zHYN}GB*<@(rb~%A+XpiM3H)NzcV)lC&F?y#0D%phYaUb0bZvrk4KFmQE0K;4dwZe} zoRzo9*$pWoOd!a0!9i9z>l%dR0KdAcRi%|8+rJ=le6xOE42xS z+#2vOo-m}4I+kS-{FUBHx{7OJk6(Lqk|A|!L%LiMupg1`IJeMxp9i;Y&VQU!qYzjP zxp|AO+nHO7K=%_iXZKyzO#Bg8AJ@EcDqXM-aiH0pJQgMqZX>70xpYotP zJMFGp7@dOICewP+ZAVttfL*GT*HiKJduq)>5rYUPmav0Fh6%mHs#CdEqt`*R7q z1Nyt1Nq=#3X1sn<)AsQaO_;TcmQ6U455Clp8D<*ZbaFg=bG3qqzOo`IZAmA<5Tf#Q zqE8oW7SrZDds*;4m$`%*C(u`70tV3URidsgk+TDl_|?eK6tSxujQT!+1N?^p9^iqQ zkI*}^6+1kYXBk}2Hch~tjntIg%Ev`lGQ@P|d#8UewJNU)JRJ1cw;p+%)1l1i43&|n z^_?aO=;&j=?JY6M!O0hZzuib!0BvbwxY$k7B7!zcs;yse9mI7vg)1~`ATn^vPRsyJ zN>?*x-2wAI0C#UP{D3dcfX@v_T!x>s=ZfPEts?zU$oe1dxpK|-*V(n*+P$EGVd#vf zJ-Mg)V9e7uz++HhHhH>i4kQp*`}6N>ielxBg-OQlmNfq+onWaQ>#`pG`hABaEZUfM zy1rlP!D_J2mD^!2c$cw-OPpa!Qe~$wb-~%6vrAb&mTN(d$ z_ef0+2Vs+-27SkfZ-sS;Gbqc~1FmSbPI8*8hWtlsNYV}4#G?Wyg0F}HdlC7H%0bM| zeIBrWd%=Ry=gy1KVlj3NwQ1{ip1}z9xA)U`uZiZLncMzgF+sQ(1?wxmw6h~HOWu1m zA8-{>tfn?MpS8<(mDTyrL##!J-#uf+b?uMyeHVp2r~a=B~*O_9-xdG{TFX6|t%N2pFXyaOZigW&R)6u*QaN zms-zi%GWD{as=Z0?k7}rYKUeluzjIWxts!ivXpx0^lXcT>(u#J=ooAP>i>-AE0E(S z=IF@1DMN>o_rh#tl>H7{vXVc{+j@$9Rm39u#flgMZDKGM2%pvwxy~7ASMA4ofu_xvcpaLQcfRS$dzgCOphI;jF#?}t z>D08%5*NZrMh)=2NIGRiRzZzH4J}_^o!+c`_i*?F(eWvX^ARbQ7VnbeM2gYF3tyqO zg&c7m66RXRB(iO`U5u|Vm!Tog6c`XFPV%+RbjsgF~#T=y7fC$-`BiJ@0T?KLY z;_eShk4fJAM$vUSZSr^Ym>+Di3edlJhh zA*DiFX483)B}~&xVjr5&yj*8b{d7%CB3H_I(^r(Uo7qg+zOVg+!i(F%(wQvOX48x9}*AT>&c^h^P{56_idmA!^6Bro1Jq^30+lDcre`(ag zQt;>{Bb{&fB;<}pH{8f)dQ#;}M{^Rj)p}JH@dl=Q&m)KraaD!I|G}6;P&J2 z3!^TOW}76~O|{_@$?w_Sb3#W2f9rdtl9?Q;@f4vrg@K)0lX9J@a?O~|;8XoC(5+5j zN{k{k{19Aud}?@w_BE$-f%Wz3sGX*zxvWDz90Y0Tn#dDnjGb*FyZGVRVOsUoR+0{x zKL<4R@XKAfV*Os%UDFh=bzF7`t3^a?qv(lv2)|U)1gbg*ohA?dvKc`XEE_Dn_I>U- z{(5L-)lXL8l)8T8dcFhUj|R|loew4~YhRgrVtt&PFnIv!2c9D6B=ukmjj`F?y4jly zmup|8E#9)ywht^T%YDo^HNypwwMo6Hzh9$|Jf1&xpx{CI4In^lI2?Erw;c~Bs24b{ zy#JLK31)kGarNzB7s z{M{YNAML@z>;CZdyr+3y3oozuLl@0rZyDMDI{EHUF#!b1@>Ix82l@PPN!*o)H(UIs zj^v7L`Q1Ymc{Ki1=_Zdv!Bi^au6uVQ-wNbmi=f(dnC!@Mgoe1{jj-qwV2#?3!KSfF zR~tjg(QKB)P&WboX*I>0ylZ}qlg&uVO%mcklAx5>)}Z_D=G)KU@D1P-+r2&0+Z5yE zR+G9RvOuPBkB0oll{+rQ4Kt1>*0~Fiqd{GgW#oWFa&5GHfRttDo^K)zGW?T>T;3_0 zvTXN|+pgf+JV`0#4!_MC$lv%F55>aYqPVO@g3V$DpieyJ;8eM&rv&?s%@1+1npUu? zwfgLM)EVJTIHQJ;)sR`BQ>Pe3C%;~VPOD8;Xa<;ehM{j?=~a|r#4P75%`ygi1nD)v z*E-&FgG_cM0{3eeD6$)S?UU{d2)y&!$!};WQ1l=2>dlQe`zUMec#?!b0luAQq=#+o zN_LASVjEpQkDkWo{F04bT(?12ZG$eGV^Lr1`x%55S+~S_H~AUNl~U561B!I3kPjQF^s#?j-YC3y5++#S z&W>NtPb`H<7?pla%+~qr$R%hA*3_JOS~I;GrWP0wn?al98b}I77t9|%9J10`BiEtV z!h$|C5>t;Q2lS*WDAoWY-uu{&-Tf29b|la?+*+C*1(MJ7lzxqYo`l>*|GQMy*8~ySrD#a>6Sd$CH9@ zrF*X(9^dx79ujU`LkO((oi}lca&No|&Aa20w5?4C1@-%P_)Pw|&IRpGIo0lSC!xV_ z$gtFJ2X{E#J8SXCH)D2#UCBOTsuIwo%@xeF84OvrVsBahz*^*CifutACL(fyb-q3` zwRq<6P1?K^$hAqnC#};D9UX(U%ug-7{M^R2z)a$wzX^Nc=aA0Q%jrODeqDcs}h7tC~7T<8mJuFJxN1m>?EeViWgwsw`On@ z{!Hns-i z?7sUQrGHrY(Z-PbYkcIZ7=ly@emnzj+`S1{A#&)#+JjrU-I95iw@~sI*iD3%bH){_ z$B=BU9(ON)56ArDUTL}kg_|K%e$+$ufv`n`gm|Z_x_GYD3wO5Q+-hWPVdJ&wC=!P~ z1&!NFHQ!|(QnTUYr79kso+ZrsrrV*QGSc3rSUfT@0l@?S|H3$r-?$ZSXI#r4KAL!B zE-rDV5!UqX+D{Dkg-hQ6d^mE4E`jgnp7rAfZIOI!Qu1b(L93EsQ1d%DRai<}=&wZ+ zZMq1Uvk8N($`W@@A4;wGU~5WKP1f&n;=z>U_G;jK-l!Bjx5HdHO1Q1c0uM`~`c&w9 z#jaTZB91YCS53@NHLU|y-449X)=2T~tAYSYdL|6%_(L;nu!0G8MJM)PUUeV&1FDaw z-?5E@Dm*I@%*v^v6QLT+sBrXSSRwx0wADsoF^wezzMX4D7d^}8?_yF)@ozAn>Y=w4 zBlIlYPLjhfW{kY^467PTt(G99i)!APop>0E%AL=f4{dBsGNX6#`s)(fCLhfnp_X~+ zK2Jy(St*M4yO@_P(c8p5n0$*fXpFrqBzr}A>yWB+6gwq3+rW*c4)-3`{<{99i^4({17tUpyapgk%9*A`LXVGKvcE3qG!hkCm z9`R%P$nFTvM6Q+hyIX<6o7_`jf+HW2Ojg)HAP8+EIN38idNHpCLO41?EeZDIwzg~ z{rV43)oK%=deKsU^%)(gR-RkbL| zi8(dboqey};eNuwk;pykJmi<@SDkOkWwg>W8Mi?%!(3mg;wZaB`wh3nq(eU}cb52F zd0{>1HJZQ3DQrgH@4zUw_e5YLY_MJntopKbV0nQ0BNi+$G)*nt8leD`bDoO&`u9kN z_m{9{{E?8p(2v9ukmom>QFxnidVsz%2DdOBRLq91h{?p{=SQpHMhU3H4Q4v z@9_$8bqm=!aQ4N0bxw;DD(_W0Aw#@>AI1RY=J=$}c`Uz0x{hcVo}yLp@ug_4uFbb$ z^yAC2&87z}pL+4kSbpJZG(kOcN+lw#z<=9_2URm+!TE6{I*o%F>CkOS#(GA{r(zO} zqP$4b!eOEv_5w!Zo?79klQI6|IOq}Hr$xXS-u&F`)zo2=I)k=6qwcesx{;(uiBqTB z7i>Qmyz%h75J;E7CvXa3ooq}0+jf*r7k^_#zS;usi>=o|Tgge%`F$38Ob(FJP=J&^ zX-H5Cp#g`PCn}tiQdx}ld}1V>*tdU4>F<>Fgg4t7&G=AhCK3zEk?(QSGyB0e}4D^(-E3m5{k-cB%K!3X>Y>&VE~R1@}G15%FiWhm8uE4XxLs z9{&qfDGI_n)$O61&Qaz{PkWLqBc;Hp z>utK@!D<2SwQK3Te3$D``2xKhs;6Z$T^cWI#^Z{JRSQER!w*i zIr3$s?^;i;0;}KhcjBKTovt~+CBE2&I}i6;UJg zx5S1m$1`FMDweIu#~e>E~1Db~VMM!xLv zFaU!Aq#ZYzQf$g!^112>2Dd10Ze1>*F-|ghBCD93Unwu*tqPQ~+u?F3@Z6(%M@}Qv z_pR8jNRF%}tc6hI3dP0=YHi%k%DYs)CfffANw{-m9IT$#Dz>hxo3;1qmf+anw(>H) zht!!o5%s8R!T-ZzX%W%6;BKNxA$|?}+Fy6UJ}^$20Rp#y-vBw6D_K)5s#oxUy%>?x z=$eAPwB%<23C6PQ#~KxBE!%LVdzom}qMYlS6-lR8c(dzX5A5R-VF+>Cd-&X$lRUkg z_LH5Lcob@OkB>>b^zcVSl#FFkiIfXYZz|dzO1vUd$m$(`{m*S5AdlzMp~ynWsbk3^ z>E4Z7>2nd`NR}}6d#Zo=XOXmz&rdJ;=F=5qvvMqwJY+4MJlcP@T|30dkmTcKSq(zx zo4#_UTmXqLdUVJ^$;O=0Uy2|92fRM1Fuz;cbx2N*9@+Tt^5RRjtGAguo9!}%Y@{I~ zi5Y@z&b~(5-$KOnDA?D5I{>MKB?Z7wN2&TFW)nYUen@`2PQ!W`PnXyj3v)Q~-Neb= zLm&6`=ms`{aAK*x%<*GFW6nw3UJrKtlPXRtUnj_t`(KQ)%Ok%gjEny8Dbj+x zMcb?DtMI%*MkDO3CcU0gN=QDq&Tu3v%L)>AoRgDO+nFx9NYDA9;v?ZIr_<=5pgs)_ zXW;tt#F2yOqzR)Pw{dMMy*m20;fhST#USYUM8JjC2tE8i=|I8)2~P$IwE=|^Zb>)3 z`P|i_FaLmAoT&TQdk8s|O)93_NGKrQJENVtz^StV;%L}s_yCj9x5Kq7*N$l*>Zci@ z;DG`Q>}z4S1Lz*jj0zVTY5|-sZ1GDTnI5HR!Ns2qvuk&c3qdGF*wtWgFI*lcbJEy5vrLxgJ%4FWe>d2x>csJge=Kp+Mw&id`vn zCXyZ%-b%GchCedv7?u}@rutq!Mj)Oc!_2ayc_g|G!L|=?;{C3!5Q8>7+Rs_%;K~Wb zeZ#~08q&6sz^cD})JNrEzujk$d}vC&7XQ@j{2uk}mN58Eug+By#Y(hOUk{sCiEg{X z59hM_TIq%Lm(LT0hF-uHSZq>8mXT3wn%ZE|4*yz{NLi!j;1@Ohf%ha^c_x-HF2T({ z&#Y)|B9Y&ZckcLCE(Xp!wf&e#LgyUfFXMSVAthr@)R@8zT6ei{$LS>Gcg7J2*y@In6N(y4-<4I8V!SvRDCuf}Da}k+Q{`5! zY`>w_ITCd%>iGwWN_7stdmO+Udi@_L`pvHWD7z~e^(VGQI;Pe(Dl~Z1>C?2SGfS`@ zYT>UGFcShu0a?{&Kl2pgLq}2c7=i}+Y#~y3t|n{_ux9hKyQUd-InOKa-A5&MwgP!# zH~#nlBJ6TU2GTRJ@rN%n{us^k@Ag+;=B&AI?ovI)=(4hA_L-c&=dN>%P#f#-t{f2^ zxz0t7y5+STatwVKj2YiwEX6a6vbgx@;yE-ARR1i=8W_(G31>Zq{6(F4-j(mH{>V4E zbSQ5cvn4b9GEo?C*grx_t)G`g{mQ>Bz$5b9p^ju-dsqjCpF>&Mq>{sK? zuKh!Z+{+H^+$l|Q*9kt~$1v)2`+`@%-7*Zj!$wOB3R`*CQGScQP`GajzHmf(}>3O{#YG&|FT z(fbpa5%!+3m3#K|ab@lXtpAMVBy1D2l+S1f%!56guxe5z*mIoeS}>!4oTcJZQj6^- zw>R}M3~U5swXrsThO)3vpOwd+@7^CY6QR{ts9HBsvjpFay>RgZE5D?ZVo*huz=~!A zeogDbQB43#@fX0%9M55Xxuy^*9bA;>#OB?2-C4(QKVSEZ>z49=7x0nowgvBv65WGTMiMy}8vz1JXfw!)H)?iv zBrZW%GD=oEi0&LNBK>oA@VgAw;>#O|_6^+<{F+3wr#!Q&={Q^Fd5)2`i_%w7$81j# zRgEdan2JWlM5>31f3vIJcQ!B#m&1lH{dV$=rV$rUZxNeg{i;VGP_H0`-UsrWRDnk- zGfkp1N!p8}{1j*BAv$i>!*v-tA;Xmc`?&6BZ=;`ZD0S_cRlf+XuIdrD8{7?=5&3)< z`xQg71#3sJLi7scrlFKknW6TYzC7%C6{?%?L6#0gH;{RsiFsZ4I5Djray@$Hd6tR_ z@qTMg8AxlIN&)g~QyacssRj_!Ukh4Ka6yLxSAetAz)@U+p(=?3@W1RNNulG7yS_|| zugUQ_8YK@%?@g1}6MRX#)ux1~{@}tZ2qYT9y zdQZ;6=6)B@g59`Al~zUMaFsL7K7z#Xtr=QaW3SWX;Z{<7qS#=Xs-I=l1>~>Tv0l~I z@1U!xZyY_fUcIuuJmM=`Bo9BR|J4Iyhkk0*#aKnx=1fzDDb$SRVc(6zphcX9R>3O4 zyitQ(pVl(@0xy>V8~BT{`9C~HBYyq|UcE&07$Vfqi7_?qEb$HBgCs?gw2!dkQTHfY##EWe?-f<`b zqq$b&!hwZ8R!grLU+eO%A!yGrN_pj+!_1SY-*ivE6Et?-Its{_+4c@}?#V3u;-wiO za!E;bB7A3jcXIBzAuvk?iL@=7Gm}s^oW9mE=s54ch?HG8Q`0HqpC;T;*|_^eH$tVFeF2I4^em; zhya1$8e1~5_r8|4Am_UqnOP(WU3K~vR7DqpeF*ly2yCJ={}526?_w!Lsj??V66m|M zs@i>q2Y_jYtNF*$%PVT{y~^t{_PIVOx1H{vxhN<*4DQ6z03UE|&P6E@+tt`>0>fZw z%%_7iryw1P9AIDVyY=A3D3sa{%T!?WdMu|c>vX8%YCe68QKhm+@xfS6@DC5ePr}TX zdeH7*R%N$&zX6!(XMmZ;suNPawVX3k;!tRuKt-%B%iL?>Ms*W7?rt8ImJ1eA}>_8e|<@b27U4OQuWRSk)F-i-_k znyN>CcaoI$PUIPY+ANt#)Lp?qZ^Z=&mwP z7=^9T?1@5sherBvy^5FIY^#9w)mC2h9F3EmZ)2Pw@FKZXLYg~|r{dP-)W=m)oCPW2 zAlWvGP2mL|MZIfK7?3iAe!OvePyEg8G~>Uv;@|GJ-N>2k3?csp#ziD8>zQPn&(epo zz5bL{o5EuR1Co46V{7|EnAAA}y$5xTO6dO`j)?r>mWpq2Jmn^*ZbjS^%btW1u>~s% zwvjiir~^Z)62dd;YxObj2{|AMpG3Z2@4kmDXoFeBe)lK2Gl`HfKbSF3DMT9*ZQi zuG9yrAZih&uufEB1=K_cF`qz@gWl_*JIS|p8F;%zx?}mMTi&?qF8U92L$?g?EHyc- zaq*j&8PT?;z;^s=jD)c1epxpxWfez!mKa&y=h=WroYVH9KD>F0UR43QjsojqW|jho zk2xIrv%g%DWmsTXU>!V(ukjdN&zPvgJolp#Qukd}W@i@;tQRr57MZxcZ>9{g0@jt zdu!sTy_X!s_5$oq{j&!Y>@lV-5PQ-WfEB}s$y2PxRT75O?}1NJit7@Wl1G-&6*;tZ zk`IY=<&2~lx-Yi(1C*uIH915=SDcQ7%Bs_Z-wz&~*gaWpKi5~1U?`P#atV&zsB3{3 zo{Is!XrK)1`i91SY+_<-#4k{c;%)M{Y4##kOIJm7-FjRO{moE*JqyXc6(TBF=b?D> z&#r+Y%g1)zkf8Ady)T-!52o%P4+pd7lyTZzVc$Eme^!gXkG}bn;1sSx)QvjVU*f*J zm13Xy>Yzh&>f=x{K*nVVN1@^r1nE0Rw&CpM-9roAvu$x3B(nEzsr)H-RUY1a8prVL zBVE6XUhh@4&tVgeQwiZzIw6f#2h$Vy)NqBJeX|jc1*mb~diaVZ@TN31|6*7ZWA?ZY z8KVExWFtSkn2ycuM_d2J}ZHI(RN7qnur@bEh}B)ldg0YNAH+OS-M%B(j4J*sEOj~c()aFJx!qq5=iunPuptA< zm2>^qT3T`SX(B1(WNQ*fdHn3_THhDz=&h20&VbfVljZKm`g>V6T>hckR0ropTWim< zE5k8B%EKl$yt3Za|1AHb$B}CjCXQ;1FtK)12fLLYy7)pjzRuS1Rm6M!v$s@6{}&P! zK498ojQ8*k3SClp;f=n#$DKI1lJ`C)YCS6!?syD(85DY)gDo6Zdeumiiy5qS)sD?XVYo0UdW{!?-mrZSsx)Qz>wnk;~0+*o3$*98C9_N7zK1*&oVX;bowspI9 z*Yi@quXKMEO?IhXw;Q{sY-mz(?Sxl(mW|xZJ`ikv_Kp4n=Rv)WKungG=kTcb#LhN3 zwEBM1ITpRGNrTDy2aDQjHnfWB3NOd6j&|0P^#@uR61|Zy+|vVQe)o88@t)Tbw$3XHSN2b*V*Qo!(h?I)G#P4zdtWb+ z($26COcI@I_H76!4Yg9W6?LxI+m0b@U$ICvkI#<~w%OBg0J*-m(}{4j=>IqiedJwU zH~#02Y3PYJ#v~fV9uOAZ&C(M2B(d&*;yxYX!Zg7$!&exzR1aq(Htx6az%& z4ba9PcAGkmJ5M$d+?Kl6GZCw2IqsY%V^)H)C0ehw=5`A}6I`o~xfLozzptjYBLl}a zB8RH7u)s99F(txQJ}&ZHEGm={6%A!r&^P_KrC23=Ty+|WAeY#hmJ?C~{ay!&+)r?)^qXdNUdQ>rfH}B6oQ(c{K_RCHiW*`Cl zhSQT59A4c`A!0`lCqSK;8?s0JrclClxm`?U9I-kN9{&=;hOceM%_c;9LMK$!a0za; zs_rUWuGNW9HL|%buDIM#wSHX2BTV6NZo*=6vl&DWC=X~sxk7VkPtK7EAw zEOS#({a@K9Wjtgn+ZV$90QN_lcvb_K#@mP&P(cDRdh+`9m(d_>E5k_n!3faA^x~Hz z^EfyZw-PIs*ZRBYy=E)vvGV;nwiSfNZU%7!<7vU(`<8iEg^5~d_&aj2fb5xHKkiGz zwr|2@eYu*1rvGf=%SV@7`+|CMm(LMt0)R-rXoNkZ5jTgRRVU$gSOJ7&_~{hm>mo$r zJqW`EEuRjci>eg=p4Vb)f%~;jL`MN&H!izKFL`OR*w@M5X+)I<_jcnmf>H_C)NF21 zxxjh*a>!a?yaaA56F@NP4G2bKL)BPht4?*%GWJ2Cc7(4}RKGw~{14aySQ$;M_Bkw` z2xj)l7XB0#)9hvn5UQ*aj#zq66)X(+vB6BR6CK#xm0D-!9sw>;JOTaz$J{o z7F{33spZ$$`Tfix-iH})Ugml$#B~ltT_hgRURiJ${@|6KlF@W7`xNfgesr;U4>XM1 zugys|S&C=8wYcBPsuuMZiME|1QHS6yM{xj&Hi%~53~w&TB@*iVMzj?90~#(nIo^VAaUeq}dpttq1GhG)2XdrVM}bUw6(Zd%%7I~KwuR9bkVWs9VOv*))=qJlOW2rjHS-I^)rolvB}2Y|nqkbZf<(`11lSj|%dxo=hwDA9d(V9)G>9*5`7g zFYKH5{+x5rL&cV!V@u{%P35Vu8e68-5mE$LnAkeMM3K-Uxb}f_qCmdt^l$Mic{}bU zY>w653{0!9?0E%R>33(n?_cC83?NUl!_Rp20=slSnvG#;z)u8BcFAzlEnHtLhEc@zUz zWcIim7QFkl$+Xr?_t2oEJC`u?(6pnBHLU1}DNOw7ulZ+S{JoXY)?=ErB=l~qg4=fW z8uvZtFn z#_0y)?Sm&B9Jm)c$4vriOrBm<*U>f-`(dA%RWmVqR?9nDUQxXa{zo&ud1-_QIC9~~ z?$`>GD>%?rop0jy`kGi&qD7PMi|~0{iJE`apeENk9&}K;!D*)sxCmIRR%oytT{Yh2 zs58>`Ze3;1vY?G%r9`+J{LNCS1S==cr*LP*hOv+5$rp*C@W*>kKha=*O{C!d58$c( z$qrC;)?CDgof!a`@mGORt_7DB>wLS#Yf)cQ_eXmFC7w;5b&O~uOz4hfGtppn(vy8B z+jA`YdK0WHmz29MXywPsLzxTBDD(=kdDBDS6BJ0K*HVi@DyZJW4mi&pMjAoSzte;{(71GmIN%O$1>Y5GIh zgyPZqUrM5&H>8~r!@p8>9UXQ`XIr7i8y5JOX1W@<*cp3yHX7s(_g%0r+yvPZNaO4q z8a}iH6W3&F`5DWHZde+9w>#FIi=u|Y)PYec-fR|cc-TdkaC329)miI--CNU9nu5E2 znq{rDEyt2Lh|T3aph_fcjPnKTwW-TXKlOADxpUST+&yH!OYwVt^2@pI)Bhjcr+{K1 zh2u658ajYILD7e)X=(zO5u3$$KY2$;^~0uGae46L(Utoq-!u2czEe6=)g6kL8kvK8G@V)}x25(i&`UH2=nN7ZIg;dMViOSY_qIz8;}%4tC~dFG(HH8EPB)(tU`(j z#sDX%af`KO@6W+;TpAnL7ncJltP7_$WJ+=y96xjqP-N;voIrya>W{M~_96(+H-E3a zp@fVoN2+M7@WWON8dFWdd^Ulcnn=r!QZUU!bBW)Totj(seqV2unN~A^pKW_Izom8w z(Y^oJE%`k`-6Nklkb?_ekQCug^+=B9DGkQa0{muYNvpqzv0-6@CKchmuK zr;rh25Hh%f?A}=rXF>0rtu~&HT*K$FGwHYFQZ_Fs>Daq2*)(>T>$M0h0rgiNV*ko| z4}$n=Lk6xnt={LwcqdFY{XRqeJ?$`*cZiP7Q+nL=8(W_jshIL%ffjg^-U5ANh^jB# z)7eJL6sMjA>WgUJxtuR?J{gT3+|7r#?zh`K&i8w=BCx^)*9h#}KjqWO`7(YpM0pR; zV}^q+%b0nKeF80CG>Q{3;EPV%oTuDpFS5SXY@D_c#J8Y+Brp!0Q>lKc8nSh%)VAn8 z+F^_}1Sf7Eu|PYeLm(6U`tfFHip2Y8O5Nct!%`}GMQmVyTn;y|T~14Or+pVY^4%q44r1E|y`TA~->Q26v&kgT9`piECZ;1^q zQrrqk`9});u)Px}Al?2%|7xfC^69J&J-0zSA-@dm>@M~lWqz*8)Pa0{D85GD?iuN$ zj#qpHdtD3SW2G>}@y(cg`WrXGRa927t}2`o}07A9B1+`dlf0O#L$v#ZKO8Qo(y za~9QAx{oBMTYsKWReI>GyJXF(n5Fq~v3h5xe04h+djB|ZlXsa9QA-+LI6pxGc!~!$ zl>VwaThB>G&KWk;_0U%3eXaFlMIJ3Z;@&<92~YUfth>}e(*a9YSMlkK5^7(Z{5_lM zT_krkl6Pqz|IFb}A>g?Kal`pWos!wju>Mi>{Y?PHbPg(5VIj3CChqCI^W#Elj9!p# z%s-9m+GWq5T>}g!6nrT0&hL%ZX#%ad@Kz zoOk{zI>|4?nIIQGb))upMqqpOlL%gTuQ|NrJ<`4R_aM&4F*0=gYu#wg;d4$P`0YYn z2)|*!@0l{#VNW-#Xua~1-J>!Byf4h`FUXwxZ;elbX-~7rf>q9dls1GZ|t6R1j zj3+-DZqM5N_|U->v*Nh)iprT*N6|J^sIAs(2eL-}umn1eYTT4Dr_U?j@wL5(Hrnb` z4`KWSrX5D4uPlHBHg14q}~b1^aWrgf*IEuXM|3`pwho>z3%|e8MZypxzZArshSY9 z6bXD!+9GioFAyruelGz$CJ@$|SJhzr+tV@VohpQpqNM=xE!jt7eDnYSmJIXNnkhWw zqy=)3pCeK&r>$=>H~>+`e}VBPj!wrIXh}=!^%E2Q(vcOdh1_r0DORiA-Xy>1 zp(1-50r<-8o)39`k}@9D-~30D8K6JMwpl=L z{~5@!|&*94KKKvYzE6BPuJDjh;mK!qUEyNLAOJ3*;R7X+jQUP?fkNS6{2>C&W1 z3kab@Ae4|k!T0-S&&=9u*38L1*lTf;6Ch9hKi6|#_YeIf&u#^7UKRkBvqk>!Ovet!pEK)ag z-S9kI1Q#t_xHhS5{mpBP!}_a}E#rH8fGSv^leJfi!5b9EljCv4*K4!xQZ|dt)aq9l zb!d4Ary8i-ZG8D(_$YkS>FoCig78;jUxE4*U4Id|)T>m6f3gLm%^J;GkL`(#Q`5x1 zW1VS9FH8(D7L#c-nsmwIfB5}eVOI%y?S?6+yRoqQ^X^}Jao{vEdYq%mR)tr}oluUPlu6>xlweO1qN3rw>MbQ4?TuE?l_ z+zDUuy1`rtds|3La7M=4%u*|reCh;0hXnPc0J5zEeh zdwy3cI1g$XJ5q_>uG^NpHgJVKk=m+xABJnYY99s;GUNH5SnDN!Gn$u^Vf9qog>T8I z@VlC51EDCKL{;$4ckgrL9Zgf}-ZRko+yJXJ(iZ-?fUJ>{2rpqYS&A4i(R9J=%V*nD z%06xXta-7Uvokbo?v-PFKXsye@83V31qkhY=J1#g3)H>z2{wB7ClNgYl-Cv>7@C_o zQ=a-Cyl~IHTWwo#;^kxJHI84;up^fQPRluUmwt4@KO=pnX6)D*Qz*;QvpCovmQZ!J zA~)|b7g2i#IJ!=!j$d$jdYd8Rg~$pz#oE(iMD^bH#o%BEHq;F1+)rf{iMbl9lK<6d zt%yhX@ymMG_aDj=g;{tK8=ShyjgTkzl1N2GV8#`sCfRZ;_c-NJ30<@!CT6i6Ge!EZkTNnq$BL?t4E3=!uhM!PakKJl-veSp*WO_mJ9a+p=m%<24SV#KoQ|-o z=(4h@$BjhGtMfmqu@;Eqezor1V$mMu${Q0$knFld4n;~gH{QW=sZQ9BI z@4mV&G=0J+;h%B59pEmxtwJ;^)0*qly9 zYe7K11OK?3w$RAkhb~V}KJ^y}p^K%?4w+Im;^k&GND4|_m;U;l{^?C}@bc~@#P1-C zc3Jod$1Md$E0j?G_ZqXUzAs0d@GUHziFYEgL6;Y$kTPWJ{ebnFRj@JY`!*qcRKU>CdztjASr-9u1 z)|VnbZcq2=dYFXv#_S?r%c$dLX9IrHr? z%IOo?QAj1XilF=O4K03DdeN@u*!zv)XlF{mV+4S$xdi{o6#FAaiJd@C08GbvJA?Zj z6Z>JCSeZVTa7-BeNvyc_Uto!MareCrqdiN8z>Gp#6{kC%0bH=%v zsRrZ2^$%~4d3-s{{6DE>KbhM z+$E6HyPXjG5%NHK2=mmV?ZaWWex^wF3h+}LkMMT$A08uo5j|WobPLAxui*t@igFt76j_!8i zI8BY7J^w-n*5vxiYBsHu8a|doSm{70wA=PmDXKwv?<&#fd%|7jK}`5XIM84bBv7{k(_EPvI7j5By{zG*$;Cl_S+ zlqPOHG>x!>2Be%)X%LB&|A}MH&z{FIWkr%bU*lz2|IzT!l=Uw*+0p|8Ac8um5G9x>mxp_pP^IRT69d z@y`N3T27~Dedd8QsD0kon5Dq*|4ms38?1U&f&}c zVIgLIk*N8w>0Eb`L)Sa_%Dli}P6(D0{~psyOME;L?M%|g#IhJVrumKQ%fIMD-%C#3 z!07Rdq3^1g-uZ1mB>Q}F&@y<{^+xnw?bo;7N(R;*$A_hwhM(>P&XojU-GXZ`07Bak z$mGl6L>}MplU{3Z!FcWA6ZgSrv^s9}n3-f8p0kX1!%@@E3Z|m?w~CRE;hgiE@@E{)SLDyS((>uHJ_V)^sObY z%e9G>-O;qWLLRWO&7VaiC(L8{jZ=6@YCG1RZjZ>cQD>P)%0z* zsIi%RIFHAzbSrI%%c0DH3ZC8V#G6 z2X3R2Ik~CtTUZ*;ZCh`# z9n%oj$*!)&_Iu#9A9B`F1TDYh67w*~@Oib1`P!@Np8eRvM-(+wBXuep{_w}yQ1`@l z+Iz>sJBLm9OSf)?G4bVN&w$F5;U z4yHo8e4oX0!W`)HaK9Uo}(gf_YNj~Fx@EZ48QG1%x2XI^khs8JhS;9 znmTnw(i6=Ke_sCjI5cs&=hMY&YU5bs)RLQK9#V?Xnj7@<|K>G~=UDif zC~qG_Mzt3{Q(c0HFQPl%nLVx~7zXxQ`@eRitdE-bxBGHNapL>2jx+hunRF$~!Z-xp zw8f74(-d|1xb2J?oc6_`ZJTSb;$5F<^OZ39(CJ5+(R~5WiJ5tG#sJ`nMk+;zn&%D`tX5|0MH11s8?+`+G&SA*tdw2Uy}y={SM(JKI)e(!anr11S(Lg}#$FY?xq? z4!KVYXs^X!$>~kjiKKtin`7=@&dWmdUPi?*Z#vE62l<@G`dA4$;); zS)nC|X-@&g68v-@q>3kN+}<&+J{72jEn(>qkK6>%UtWmM0s;WFK zfiBLf``m9D-eNE8+{nnm8&+B#a5g^Suv}Z|4v;!|tYneoW?usfg;ZVVTLdHul0;t4|2z*5+f?>|Luv)08^ znl838J=Pz{Q(C;YJ>8_z_5aq0E$zk%arZmkxGP(=ubsefThZ$Z7lp>;lQZb?u^n*gOz*_t5$9(mBc+HfIF3OFk4bEbaX1n^S(ruNdh2jO)BX*SHpukt;u5yG4 zPN%ugI_h#bN7l@JTu9%pg0@&F>=Ube`T6siuG-*P2j(^FF3s_t?}eo|z3?6G4`hro zND9ouH3>g?TLtIirBwLKHkVm%O)T|9o2TD0yROoi)7uukjqei1gov5Y-|ye!GJI4| zx`$Vf_dfeWi9CDMiH!$b`=FC{2R$+ijKih-{_c=dqkPhvrURu6ui@3L9xoU zu@^Zk({`DkTCf9_k?E~yM`EIo$$UJXS?U{%G++Rsn>s_*7 zIqVvDHc>kK6fL>Ui*ykTJ+jjmdGLkrp|X>v-&CO}`_4V=J^r}E0jX}}y}yGeVM4dH ze>Ci5=&4yZkGDeoRq%gSC704&h+rd9kE4kD{)3Ub_KLIxuXI)~IdWLto~dK&j{c+( zGq`e&p6@Li|HXCp9&;qx{Mu<{x3!W(B~G=r@eXIeP`}vU7EbHyq3-L= zKXr0edS}f~-l#zaa{xRSV8>^1u^94r%(>J!J(UL)f@;MS4mlc|hP@L#B>2+rwjT|y zm+=HS2T#r69tLk7MbR}2S*EtP0x5LYt`yS8J$gjTq^1(3;dn7M>I%JT>Z7rUOG2BS z+_PzT-IHm}sig*&8RPlIx$K8$S0`{q&NXI+4MtO+g#^u8Ex=aJq3( zRjX5z$$~aO?mA6^@_H|)>dBP^56;gygZrP+9NZ5Wl6yuoUo`4cD6kWagJ ziQ6rwjsvq*R#!CgR^-SnaRx4{Ub}(!5_r3(dB%15m~p)Ba6cQ_0~(W+)hjWKkca4J zi<}-uL4IGaPNc0MuDO%Zt2d!@Lv`@Mcrrj4>3!gKaf%QtYwr@5;IRTa%lu6E4z@f? zWPOYaAg4Zgz}1VKqb~moW!o|L(9D&4Fa%2O( z7xBrtB`4i6$l~|L%iiV*womP=>#(eGFHU;-%QpspE$;S*EiINopPF(fn))m&e|LB? z@*GzOibb|+I(@NF+L**LEhkHuAxN7DoZe0Zjw&~9^2 z0}GlpBJ`_wbOx0ft&=wZGIsZ+0m5TQO79)86!>1c8+Gx<&-094NeBrVy0uW;%81z^`=6q+4z4Hq- z*b;~Enp@N6eB-`T4Gw)CW}LTs@UVy!wBfYfy;=i1)Yjw!zx-ieHycsG5wV5k$+U)H zA1<6a`3^!=>t2#&2Gm@;>NRQ93l!xl&CZ4T#N+wh_s%=PakNdwOLm+-ZScvwP8E6% zVREr&?wHTUjW=YJ~A&zrs~M1t~yd3xO=l!M9mXztv9r`cE9 zJ5am=ZAixUozlxn94zkD%pD-rfRlYAgH)JO?H3p*k=!h!ztnSRpGx#$z$z%-a#ANH z6%^4pEjV*X+CfUI`%viJsrP13{%nfvfUYDdOja*8ubO1K6}ubx2$#YS3E#73r)X6+ z5OK3k&pH)Zfwe_=bo$f!z8Wo76_8Ramja2Xlzs}$eyxN2J0*v=WOwY%%wqJI?2=)vA%zytf&iuuUWZX79%PGBriqiMdGZ}&6K~ean z$ravO$#2#96NGldi^fDmH1m4tYEqGx#Hy_(_s?BH_x1x`cX%xR)g^01JF$q1yn@UDrSXi6j%4|L`k}P*x9!*Z9F)B+ z{V7ya>RJ%ZYSn7!OQ}C)vI8xZ5W)-qOq64|3O5}I{&F=6W{6{iu8Z4^26~q9z-B;qnb35jHrM)$Ix+nu8!i#>H@GB_#NH^nGk&5#1$S9g2efpFjrMK~n`IeL~uxU9Y8yI-= zOdiW3>>NVMAt$>kjWHsh&iA~vqy6{!2;70xd)9SogxKL*#IucNIY!pR{Vj)C)oBfC z*>m&VlWe{_YTaW9{=hgsAMYW>JT|ZWrSxnh@Dpwu{F%sP0i*e?oM?i{`n?}w9e-o@ zkv?j`*X%e>j{S6Dz!+BR_g*6E^g2V=ce;MDvs5xh&8Md5UY8ubbYEr&#i2!@TZ1^1 zb!kCeUT!8=O@s+RM`Z4HK#=2Mv4+NPsieMjuw#52bMHu6n?F4u7`NEW6ultoY7m*^ zVd(*IzXEXrNMw~^m)uNJE4)LmbvY4xR71WF1{WsC{<1r*nWFNuiZ^)E@l12_IpiHu z$-N?|jrh?a+p&~kCusv`y6^CrT0(mo=+@0VU;!B!%-q%1>j6$7!TO7_1NFMM?V_ot>Cu#$7lbFmq9l>RPJXc5q%4aZ6*y z_c=}erwd%q&XsPG$q4rFcHN|;FYr?>TGoX@(2FoaG;wj{e* z1@R$`*@4ocQ7l(99_D_u8!4Wz#jV^uvS9sCqB}tY1LVRe1b%lGs0?oFc-fSeo8d)~ zh1H(sA!V(?)d&UoZZgQiQ^thg5D}%viK-!|H;L7v9EXn(#oLf6Y2_Ssg-gF~Lu>PA zdVO=cYpEoi+=)AqD^sU(`QdG0WwW}EWox_!pRa$y^t~}6aEIK4r#sb?>I+O26l$U$ zsDD$&$$PbLA|o+%2iv~?5I2e;wmh#-0S_?@okS5Fc<( z*U@nx6RyLcwL*4I33{`MN$#TJ*c0Ow#YOHWKU?GK60tEY%R8eZ0h@~X9v?M$B|wR# z`ShL~UcW{;q1j%VF_v#nfa>9d?aY` zKTgnJl?Td`N+u>J6~K{taW=lbiyKm2a~RN$U^+?v1NcUWr^{d{Se0dtvLd%$$ILqA?)eyA!TBU<#09u%co@Lz$`c{J3!9xs9WRghr6p3PLXUVQN>R0S3`X#TT<) zkJVy(6B(1NeSYV18L2K^DElX6@9K4#H4%Ip0KRDz7l`dj{mJdX`3kc!&LY;Y&5sJr z=tc|iOYr@{hzeh0jmIkI%-LW6G>e_ECK4EMgn(P7U3(*&4`!6sHsP!>A#<%mhp>;S z>%8)cw;gX|QM-5}@lK1UMdc`k0dG%`%Z&=6L%+ur?8CNGr^!J9Ptq6q>Gq|yBLSvu z*U&W(2Mn+zQ~OTi0Uqwg&!zrbcGviKVZ|LA!X7OA2g*9>ySWRqm&Q}@<>~liv`oYJ znt1O8#EX%OhOlJXSrsPpLev`SGuxIbcLyGlfv;%xpN(3=Cvur+Lc;d63-<2i8Tro+ zf2HOO?DVvnW6;cGo8oG^pNs9Ps}eM~TzYvhFIiWPb$Fe`vEFd}f?Ff@_0G=qaC5dw z5CU!tbJLXm*ntdE0fP(SNuf`n4?luq)InXfg-U?Ru^H}N>uLHbcuzY59kjRe!%yFe z>;t4!w@8!k7v#O-#&a7>3k$^`zan{?@{~Ph-gi$EB){BB9cq`as$U;)qly1+CV76NQ$c?C3uN8ao=I&cHhKIQ6NZwY{5GvZotgS85BQcLA7h~ERu>nBd-+S zcej=sa!o&8xQ4@O@7YIbuoJfK{9R{1G4cdk`P8h=6`Txb)Cv0`v3*SGQ~i(Wec#`d z`tB5atp%v&CBrAbg%Ee<14QlUVgf4qrT9gV? zxkV}@##T=h*a;DQaW97kEd<(&h@z?BPGwx%VKVG@xZw2j^?9C2KoHHV)l)xgup;>8 z66J?L0_*~~hP|~Wt=5a4(7IIIq`8uJdK|V?pe95zys-1+ zIF7-u0JNI_3sQ*(;0WlhwQqBTTFXj~+MrXGb<0@4Wj%_NQDD^~>BWrBsv2tRz>LdB z#Q5BOH}*iKu>`;0O#QQ?KGo=(w&HoiD$S?-+glit*?o`E6_-bHuiEBP zn#0>P^zOkNy+|cJh?b@=Oi>JcaTN1k#4(&X?`ca zTzcDjuRrb^pF>iXCyuh$qc}Qf83p!61&G(RA#gf8`m-#-s}<$c?Ey6Pev>!VLf+5p z;cvJJmjd=GeiQQTDGs@G>h!p*`^e{QuY6c5!8eIv^^qEz)(;5bgR1H~LFr_?qo^8;xu)8u@;JPR2RYmd(bCs#X zUx62FmbC_z6Rk%3tq)sk!tZVar&A4xU-xu9u-S2;*aq+_$mJV8@JgqdRbx=+%s{E< z+@07$xhfC;5v?o(y@>e{w$}&w8iVDBA`{gVZ%Y@2-;@Jl5n)-dJ8olkiom*{vJtPQ znm_1gCrX}yfpIUf**!Il3|>0Z{&vq$ymD}4Ou&Wa=ik1q^MZs((#30i?we)%xngc* zZKn;z#McP$EE23@6;{vbeR=WTof?XbeR zonb@0x$0}Sin>RRkT#iHz)u=VmQ*n3+PfxjE}sc){9db&rC9N@l_p6j_K3tY>ew8^8H*!Sy1kAayB|3- zesq+AAYDgTccY0d53X4><=4zS}aPjxN;L5TT`KGHeAZO&= zj{`mCCFcgnA~scm&clE@#-A520VCRwUSjng#QF$)eVuyRq|!nZ$Of}tA0x!KfKXI@ z6ts7K?gq(*+aBdSCQ}y6 z(P5qyH^}MAuI= z_!s!x+)fr+O;el*u+F-=(O!bh&J9_S5#_1793*?)C+<=1^!2|*pzY>$2V*JB>0;l5 zRR_VxD$vh2tz3`V$itFnJ$WF$MIW}a5Id*v#E0c3D?VVBU@xD>cwk3m?j_U6p z(J2&T#koG{8l81H$ycix%;QH!!oP(cGaT*pWVYS8+O6X8*LYtSARWSm6(1D1?s*fU zS4yDsra6L_2wkoF8DBTXtO|U}K{PA%7a9;(-yAHpswuSJbP1$ZZ5I8@I{(8yHEw#D zQU8~UMx{PJN2E~~CC!^9_x(IGwQM>!ZQb_~arG3d9I@M_Uln!%rl7Fj4nlX8LlK2lU{a6AKI^uB~h%4}dpU|$zqyRl4ZYYCZ%^sNg zz$S+er6B>r9O6oBRu5Ksfg%rP0HOKomj3TTVF$9%9g^u)S^5z(pW?UKKy&N^JI<$LTiv?o)(IICrn@@5G3C+Q0u==uRG zvxd?c6uksl)yRQNF8u|a&ks%7kG2Ay61ykiEy>%(MKUs!pV1bd{k@M;uHJ))rwHc< z1RUzMA6xQB+_w$ew~1~JJ-~8XHHG&{vZF6>oGb28==378;vV?(7nyq^iVa{QNG$S~OEb3vK8r zC`4gT9;q6fGtj7BrcLy}4Ah#R*oI7a{^uh(b~{Q-h)@8}V`vK6K#f)v_vCvk@p+?T z7BfLJkMe7Z=_l7b0*Ro8sycf;TIJskHW~%1LC;~FN@^(V?23f@2r61}$s)qtqpB=3 z_zQ)M$bIPfF%|0H*5pyq1+Psur`Q0{}R%)u3060H%@w}+fFKqp?~50uCFfeGO+a6V-}I~!J0Gd?DRw?e@if* zR>M^~p0T-K%UK1ck3u$u{)6I)#3pHKxo-MXscPO_0`HVZ`e{Hz*`+SiU`Z?CSjL<$ z6WJdCtk%3c9AyfBX(`WYYi{J$)zz$3!utKWb6UE`)v&?d$JgIa!T--zi?m=UCbp4^ z1jgA`wdpEcZ?Lrs9i>WS0cT237o~I`6oj)z9!AS~uc?Trv;C|L*WZZ#X+oT|`dvg! zuX#4*?{Fd5-%PU%4|$7s`2lf&))Bd#^;=`N+x?Zdw2G#v1m{t+LfO$_T?%PwRG01Q zI*RgW`@-p!YigJ&F*gVo>d#I0m*=Lt)PZC`St(@oZ6I$eJBkx_O3c zHGLb?l_nW0g-}bB@F!4wRVajbqekuQKGvjWhP0U$fcIv?lawH7B-t%2V@-FqRxq6w&0S8{r^vZLHr36dvt4ULY#_I>l(er;+^ zQ|@3;k?OJSw4=jA!{k#O3>c$aw`^!yYccAYhOK0JScKoh zV=9)DwkeU~I!fP{VmwqW-8aXTAJ}eQybrArpEuGhRi$OKqTcb33wWIpH6N7{-iH9v z5w;D9cN6X@4ZTlzfpN4{bPK6w%%mB_mAU_p&{lz4A7+PaK<4E6uohyj!27f&tQFX5xU>OcExI7DDlC8v|2(AFLZ7yvM2brAhalgIZ3jVdr~wq{&(x1DTVr_<767;m+y_P$0|$ ztP@l|4NnQeAr3l#wf~ z^vnKw)%Xv+{_1aCKJ5i#+EDJ$b7a~aBIW$N#@9;X7f&-j7Z1}csnhTeT(k4U^udFC zZ^G#rv!T~`kAg3vZwl2_Ef*iZ?j+>^ZUv}`{blEXyhqS?Xa7BUb&KL`q!04nvE;%A z%kpD1kjIh>omQWHT=5D(L-Lyhd51Gb_Q9Yw@PAS1bFL@%;cPt)oTfjH74DG2Pbvg3 zJEj?U51uNNc-`U_TQ=pY__eI`&_5c_Y$mtZ9!haHGuMRM?p(K{J8}EDy_QA;l&qWl zRd;duEOgy85@Bx|PF2qH27kohmzf~%eXCyG%X>T2_74ujZP`@$?HZ@b^vP8Jx#KQz zxv?aaWavJ7QkG6iONBkd2SM^Su=B~_qrdo5MxF&YMIWw+PfXk#zGo2e^T0?!(JtIjcM8c_hBObDWk3X_ zeYY#uQ%BTYVw|zdRap)E`Vh));0P*0gjkW3lla8faRoLeO|O0xVRVZj{ILAB)gjVt zsmkO1B>9%kUb5v5E|X?gl~{Aoa0q%mYEA!r6SvhWB2T`eZOP&WNMmrgEv7l4s%+6k zTHZM*3e;6e|9%TDHYD>bT+amNGD_7%rk+zAN}92PqJaS4ukyM5{>8KR&K-Bj4<=jb zugKVww;X;C126}RI2ufnS3v*eo|3fZ+*4MAHT46)8hY>rMywzfkKOa&Ox^=MF34c0RLPJ$%$u_7PXBNm)`WRut9x;HVU2mYW z&uF+(=jv%}KX`?Zq(-!EI@$dsokRwoRCE6q@Yz}n5#_x73$7&2bhHZf6;p)FTwpE`bviUxpwjLxK?r`ESiXy+^JNYoVjHW60 z!_s-q2m2YSl|OZkDb^+|8?WtIS9yO+Bjg4uhm5^lu1}zHCf%$gmE$K)0>PPIf&XBO z^42Rxr%DnyO<&7Zu@1T`&EL(Vgg)d4dl7WflQ+VoyS>c(3SZjr>{XTr^}C9HU@jMw zmJ%ui_TG=_kSF}Eo4{%$tE`kwK&h2DRCg{GjxVmfH>#XC5G+Fn>Dc;LiN%++!s2*%;y7hJP0I1bQ_n+G?Hcq#z z{}D}M+LHI~Fl2wzFZ=L)@ZHXbsCy({ei?&%5dmwg1LMead~6jghDGrf57$YJ_$x$F zx6G(;jB4&1vi!~?_SA)7_*B{KW_m|Yk4%G!@44ch5pR80F_LQZAMxyQkGYE{0lX*t zxnPO>@t5P0pe*n=-JOdH)LTf+ZU$t{@Fp9y^Wkkjq8l_SfJjqxrQ3Du+b9L1{wQx+ zPmlz?4bDv1IBCjtRm1(m|76nxZMh^hgf`wULHZ>ezRd4N)#a%NObECp?}k9y&GaG1 z_Bv#eSw_n>uvw!BeWNT;EUZB^0Nd_)d{xpx==A+@Ac4DE4iGl=cl3GgGSV8lscH#v*=@=PK2hV|z~!(5Sk{dn_O&Q}b< z96y0?K`ugI6$A2GEe!>TU9FGN__HteY6)N<#+yaPn!(2RsbtIqAB9BqF$P@*B8#}8 z+;{Vd1#2urz|oEfReSbN8+Dw@e-f53?>1n&j$I~`=--plMg2ddzhj4B5E~c?^jhkQ zwjn9Tw8M`-i=JcPfeWYAHK}VmiJp5hiwA}Zm(@E3`Akod_3NAGOYmvyBMuIogJC7N z0iZL2xfwiramUI7g<(#7o0zT$VabeADcwe8iY;xg*N zbvdl|RAx?^zZ}2iX%NOGlbH<=(JidUOACv>qwI^GUdVW6`+`uB2Wy=jE#DB{v%5_~ zoJ35pmyrhWv^cK2eZ;sOfLP4RpVKXByl74N)Lx;a$z#G3mR#Z^Vu~X+3@dYfCKm1c zUq}q`H5&@_02ih{S5+O10SRX_d1xsiBQ?-<_SrAC(4w$FV#UY7&J{{8J1H{R{Yi0C zA@=b86=bQAp9zcC(9VuzL(Wz90X`8{eF)_5D@gKAp(z5lukp;-S9&x$w>Fk+#{&3a z@5d2$#hsIke~wK%(jgnMj_Vy zz~OPkg&O^pHXgB+j6%O}?fnog%LT>p4}g)RLeTS;O_&$dqOS}xR(EO;)72qWMC_fj zZoit$!Tr?bn#^HdIi3yEuJ7Tgek+~&fzIKHo)spt?TLos(CitlylGZ|W5wMSvls$v zlkTrmpT;oeelR|`jkro-AQh&>yfgA4t*C`0X{~J9$lA?pVSlCF3YhY}9JGd<*nV`2 zK*ZTrNCUIVYLN#}}gKZ-A^&m;h(= zw<6H#eDDx1`cMD+1=AmnSkHD*j9n;j_*f-4=!q!45 zC-S|i!P5{OiXj={zgZr!mTG^nt8vqt(J@#X-e*-6wZ&Q1)PvkY=?sW6#Tu<-Y`Q&@ zVcV|{3K)FWuowm3Y^JCLoH4{UttDz?W663u?9{)QA@z40_V2Q2NZ=XKzMHnm=ZOUQ zQaQxe9(fu;AFlpX*|5m&g65nI%!>}lYVW!!s{#CTSy!7)NBsEyIGS-xT+!c#q@4#r z#rTFzB|joM9sG3Lq0e}>jX}wN6isryI_be>bpAj#2^9PDSySW7xZTiCPVtpfD(%jG z22>}4w6=!)fC;@y+{N6CnEoa5Me=Fid?yLh&mK-17`ZDTv3x9A@xY7cF)lfhX2J=|ZwhAKtC=VL^x*sw_Rj9?Rz7g1ys+fnS^MmC zs3*A?ZSh;TtVjdmy2YcQ zwi_L-PJ^^K3xQ;A%$j_aiNA(Se2E=j>l@$qJJv$r@?+fL`2)?Zscsi9opJwKyVK`| zz1_t-6mN?cCnEj=%RI@3)?&tvgbfF2z?9tnsUbeRyn~hr0Jy4c?c;z!gu*zuDVEhX z=dz>d%JK6l*LYfs3cq>v)fb}XW|nqm;Y^I29dP9=g}?i}J9QHF6MsaFnNW@ zNw>bR03zX{m4D0!1Jt;Fti7LWV6yMKQgZ6RIW?t`=cMAoW$~neXrWH^JF@$!rzSgR zPgB`B^9ak(&A<;hc}ZvVzy2?+6vw~%FRdi~rIk3P4(wwtf@6rh!*r9d!u52Gh6Zh6 z^m?s34YL^Fy4Q;iT=fb6N3gG>v!oGwTShR^S}M|Lu=*(rgIn{=IBakQPa?CD>cWAQ zIADC@uB+hFuANOxP(KR%9#u2eeY9p_GrwFf+2bmhk=%n}>c5%my>wAGGHheIb@1Bf z*&|cLM%t*w!>C4gc-Xx;u)Hz@!`bYi!#Fm_8SFyP)y!L!F+H`D1>^HJKxDyv5B3Ie z3PA-V6Ul;mpB%!{m%t2E#?@NmtX4TKa0=dT0oT;>)NHB9axa2RG6hQLSY+W zyE-VK2~L1SXtEjZGG*@`=F+;$Rh8pUs$m%7@bf$}LDS%@Q@5)v{NX66JRli{j%FKQ zG9R4hby~)+BW%B)E<7&@;rmlxnq}r#ZWkisVN=XkBx*;t+tXnO3KGg&UifhJVj=)Q zMsq6xk7Kva)V(iKjrzY598yvM!GbUeGAH8XDL;fcClUYs2h%i{bl)64v8;}e8!1!{ zxywL>V`Y%n&&m)FGYIL6{cL#|eEp*8OERB}f?ro=UlBWUoq}i8&TJC^3@5)7B(eK! z7hryvO}6#y`OAE;gATT5 z@tOIY9LDj2DtPJM{`({LHEu%azaMxL?*FM*e$^|(G{qP*b-hqJ5@5YnHLBn|v-sqG z*Po4{!i~N0@jmxz{w~{5m?v_ff$%Sr{1PM!J(+s%?8e}w`IDT5xwu@H6keXQ>ZV-d zL&p()5M;Gn)`qeM9D38s7&&@%C?5p93HTj-bWa>Kzu6kw2ju1sAx7gis`vWxvRo>V zeb$p-H=*bMp`zSBpq-!VqbN=;9Or|~fg8#e&9Ocs3Ec?qV^z3%4#Z?b%P&O1!D|-T zWcnlWwCc#(<@Q=ys4@k(NLj|oWSMUed=j*vxf!v}%Hz2)@^zs(;hS$inulZp?XFsk zJixd5Ho8ZQgXrPDD2>aSMVjN8$cMw5qEBXAJ zFnlPOkncE5OL$X3c=%AVwHIW}l5jmhV5pY5A%5n|1WMLUbVbQ{_EXpcM(!wv<^502 ztJ)T|grJ~axVi6IxMc;BHuNgYxcTw9P4z%&`K=PM=X z!P+x!bTmQkR@$hXQ+{EopkEbE6NJSOSzNXijRp6A(_4#P62J5DIP=^ANeb`Z<8AuWG>>-$!hiMS~Mwquzh(TN27oH>oWe)X4<0aA}`)o~<{1 z3F9Llb_+n#D(*0uB~&9{T#9{Ibo&zfQ4W)D#sZvwd}%yl_K0de2Zg~j-9%0I*i6&qDC3Lo_@t@H47f=&}vw1itf#uzR?hip(!dw z;kX>4+O<*I`+Gf2+uGI8q%+>`#*p%namPKpftz6bO{R!W8N2AzG)nst*u`F%Qt;d< z9QmOi{1m*|8HlZ_04c9x*Bi~;JB*k`o5M@;btPimJ`lp>CtE`bZXRN91Jbj0j)Kdn zcD$Z~CCTe1_#@&A6!{qlOn3=hEz16y`{lwV^XOCXkSRaRmm(=ndb|DP>+PlR*P&p8 z4)Sr25357&nu=jSRXJe{#rW8#2H*&`ZQSQD=EvlUz|?IB%^0|6P5o<+x8EE%$wFRV zWaty)Zx-^!_&Z@Wz)WP7Yle^);p2JETqZvq>=!1MZv7D9=bhjc-S0~p=vCGNd*^k- zI>qD*ug)Zq=VrwE-(@?+Ris3&g@%Zt4wWF(A2s`(s3Wg+kN9T6_j($GTbX}rX^zIs zN#0^mh?|0oV5}ljiy2us7f+Krjub4qA6`BZ;f9{9?=;SJhh|Atg*<~A0?Zuy1#|5p zQREnDTnwdr)yby??j5?Y%i&b4EoL3c>S7le2TewmbGm~WkVC#^AtNwduo`xYzYHAS zPtn*h8ckbrTvDJ_2vuf|wAU#=hthSZ6@2?NpplbA~qy9u+dO z`i|d?EMq6Dd5kP87WBJk!X010c|M3D_&aVI3<5!`J@CglvamsrPets(QW>%H1Y1xz z70$Xyf2>=+V@fbqQx@Iw2#Nd>Fqzq$`ucoxZY~FyooL|(Ksg%OH67bY+5vZa5t6H#Zr&TfyWVXLr)-v9z9 zojL>K&LYiCwtd9R!h;By+3=1t%Z=Y&&lB~G1I#eQ?5i1(GhLM<3#e~4 zf-_a9Ryk2x=W&N0)N4kc1~%iJiL4NQ)-GYR#UM6*!GiO12GiqF{*NR@0zC&pX_LH# zTthLmWeXzEI|Z<<9eRv?hNX$}A$}e-w0PY8f0TCZ@l5`2zmlY!if=hDQIxL`C5DmO zluUA}97f6=1T$T>O8aT1cqBB7$0nZv{q8XM-Y8Jlg->UsWrUccv$=YHLP z-uLJAxnHmAzOL(iectculB`ds+jqsIE432d4{gItICpUjBJFC5rKuemkQI13-~>PE z-K}dC7W}9@h4Ef&BB`>4)L8)QwU`69Qw4X?~`jVbOaIa6jzt^tK zVw+iz9r6J>D4_i-|Gk%tS40SQ{Dm$*W{cj&G^xCo)fk)Rz#M{hq1qIE=bgm(~L-w_wvk^QS&R-n`qK%jCz#w;9!uDqo3%ubK{IliJLp%uM@P zGh0t11MZKWa$TZ@*&NZnQo1bD<8@Fgp*&nmtnMOlVP>ngJoX&_t%`rD_vz@XxZ;6- z6&xbfu1QZR0EZS*2cb0&5n7eMyj2@*r4HchuZ}zJ1>y4J!-!wL?^C*DJ8ysy&4+>j z-;mk7BhlI0oIUNH;TM}(xZ$l6vfNf2WQ{UH-V#?TsvMmTZnA(V1yECNb~vJ%E-wU9 zqc3+Zh>ps-kYwAcBCAm}Jh3Tk*J5BW; z#tk*e;y-?II(M90?z9b|{fvz6t!g_SI7T1sIbe#MUR2(smJ&;D*cW7E_z<5ZQUY%%JD6m*AIDvKSh@7-b`abtbzfXhEc<{Ju z=xz(YgFZ6tBIxJ1or!Rb$&7U+ooGI4GwbMkjE)BzTF4A*pLEuEcA+wQCCYuZ##JO}fRq!j?*SqdC0vj9v%z$O>YZpJVG9rsy^x&b$) zwd5}r@4+MIzQT*V+Lpv~{kt|E3h+P>|3aaoN4Kb9UBu_9&^sT~4k&~iMM`@)PWVQp zeP1<^9zbH*dKb9TZ1^6W+eM)%QRjaWU3%1OzDe)7j9L;|UO^@7`n=dISW0K;F@gO8 z53Lc!=jVlvM4ITdH>wdfY7a>7p1!BcoHi?*In^qu|4DtXka@91Ksaz?N>z{5{}DDy$eGHQ z{MFbAxG93Va^STvrLl*Oks9E){PK|7-&8mMETXTv(3JW|hdLU0;engv)?)*!m!;AV zoT>ZG-RdewdVu^h)8@sLATxUSSFgS;GH_5>luB8d{qdC8yy22?YER%gJXc*gsoA>4?HJOeLlq^L_RsQ@|kS$K!jwY=;#~8pX~VP&Cjy zyu4Z-A_0#A!)-PhWD_{i6cVyrYP66Q5aYntb^yXd^2RuIG1;JE2V7~#7-ni_?f?;| zTR2)vKe$}zv4K_Lo&`$J@PUFOuqqoi>y@NJLi98Eq+6`L_7ZEK;G5jqp|nOPO<$$t z^ZH`W99g%J3KDxhqiR99>xTF4_odTJVpWkn&n|H#+_5>e#!XhBZ_MJSCx68?~{g&N4s+bCE)Yz4BkTYA+H#I@FPoRUl) zYcRKE*THOFB1}Ws5p}R70_J_|xj%Zhmk}YpxuhG4X7j6JD$uiPrR*+apXY^CS<>_`wx&Y9o>&$;ZN z)E8WOXUKguWjZ=4z2GU5P-vZ?sI7lx)6_F7y+KULuJlRcq1dI3U!{4Yl*73Tc>_lgtFZk$D2LyvtVjm%Pp=hOT43|6w{P?%`jB}W8qiTNC>Hj zT9}*TuyR(CZcxZ74GY6o4K$D>6{NwrXTIgF@^XMVE2fH4t-ibD*$>AE1 z$pRL}&mia6BJH?VH~B)d!}S+_zD1E*h{Bk_`+5X1K2EvyJ8)fSx`+*Lm+m|rmDWC zz($Ar8N15r3e5uga>MKmdMBdK-YV=Gex2|yVGEsy&fSsqsW%qA`uQnoieE93zqlh) zXhAnTD)X7&{JZfejpY7*if0yX+nM{MbgW3K-kp8P7_9p!-P{NaT2REo+;7X^w(XjA zWE?=!1j&64*25^oC1QO#rq~ngRMTT>2o*45AMN~DZ5UdORzoXH4V>~Vp2&iXO#bEkCf>W3Ea@z&B4@aWC)YL5fAlT#j3A8RO-tLH0QXzrY= z7upm5o!n?R-+rqQlaIi!X47>ZUBfR-GyUc-FvAp;FcG)6#fkLGCiM`QCOvP49E$e$ zI=a*xD--;VJaNTZA}d?hOJ9IPXYsNtvbl#kC^OgB8X87Km>CGAQ<)^reP=C@DtYL@ zy`jhRBGed1?tG3VSDwlKGF=X@z9V+VvyXOzV7&E^fXW8MwMaZ0NdUE69i19l6jeZ( zv;ep(?Uw{Zi|~jb9kXkEd{Bkau&uiO4AGVh+D!2Z&#rz0Js1bd`KEPF*z3@K5y)L= z=gzPDBoS)N%pnapxbz%T-HPwY&e=45-&>J^c#mAL;_FeJmU4^OH=5l}80F|D4Ruj~{>)wVc9?B5zVh!&$6?v8aikxE!8pkrK_)pjmkt`zL zi-8i3m`7@ITTsf3{G(DjM^(-~qbf-WD%Dahrwbh;WYQq=ekJzfVBLD*(;gu27w+$F z1&CakG-TYBae`3qeag(1aJX}N2vt!BG(jhtm%_i=?Vs?K1R`{F=AApe ze{>M!-^ixw?tT493>y`}y5QMWgs%c2osvzN*&V1edBnX*yZ-2A4iQo9Y`pd~`wW$t zsoQ86$k**GDg2qZdMYn>ZGT0v1~$*Oj$Fcrwg3|DquT#UdVEhV^gW-Q2jPl}s_5j)R1HY926yrCUO zMZQ5W$N9(C1G&o|oM@M!{3TU8tfBm?n$))>qmw(|@v3e%-UTQPLIf<`lJgY=Y9M3s zjN$RwuZd2=z%VYf1&>ahSk}L#3@Pcwr&M>}iY; zSt@1IE-jSk1Y9o5?OjV&l&lEU0i}}pj(v{Q(@6~q;?hixoYtk>kb3; znI6s_p*O$T(avqj5##o3n<0}uI_a#oCS5`|h&SLdeK#&M_YbN0-{twH>^90Dz$DZ0 z9-_RXsi?fPBe1`!YWyGK6v&Q(mYwfes-s*pVIHOKVrCLVY#z^JVm?*zNaEb6HyVGN^NyQqg4^%iW2b zwIX~6g4TQ0B1q5Esl-64BJkRT6hXC%Q83#2WPP!I=m<~vO;;$7v8=oRv&ljf0g=2@ z1@~ST6$*W7*ZC^QlO#{s@VyR|Ft4aEEcwh5-@xF=!{~(9w&0$NtZM-Y+vq$$-wA() zhnK}ccDe)TD7pCJOGF7YC7P?Bw)&VZ4DT%3*`Fy0HrKbxlYO^=BZ#H}`JT2B?GK36 z(a$?uM6)FvKW%SuHn|OQAK&u`S2TJ zgr_*`pJqukSwE9kQofvszt&9hDL@j>cUQXwG5?v(WJ(|>Wu-rA4Oj-Dbyug; zP(FjBLDC6JfXG1Nre0e}9)Q2&-!+H`VCicY0tQr9gmF7v5T?FS7w%j4UBePf0 z*UQAQo*J+gi#Da-#xTRPd)5&@9E)r59xl>NF(eg>Zyl7B5H*po?w}78@jQXFLo9fI zV_UgU9KB&q2X2Z}$=Hkudo(kU>Kc9|M`HfFZ=~A$Y{V|(@6*7(oUflRi>L_s1wWki zLwu5I-JmoMSz6DFWH^Dg$;DQc2$fPd(1C)&kH#Y-hj$rnNmc+(fWk+brO%y3-~5AN z3$#?#oBV0FmPA5&*%$pd{V_@}5fk|}a$L(Ls0#tPjq!{bIFjRWjIWlo2|jPm?T{;dQpcHw|7`gc!AE#B z9pLtN`BxG-SC)iU$2d9aL!eZ_vF7<}SV07MBZUsTP$GWa$VgxEK9ud~`s=RD8*zTaBIxR+V8_kCY=|Mzc4eo#{&C8i_3apMN5;(J-m8#iv3U;piKkKp>Z zQ^0iL_1jH%O@((iDn_5|USHg?mR6O%aicn(1Y>sh`kKh)y`KAx8)V&o{oL$#F8+Mu zhM}dRthAPo@j*M{BSU`@9*lp>o*^8W=z~QooLvSOB^e1uOGfU`^(36@RgwJ(brh6ZoXNPgIq zW{;Nyc3)<@xA_0Eb{ab~cA=uKu8#LZiY#MU`?E2(3f?r_7jpVx`kR9> zIT-v1Cy|NU#Rn&IXsDgHk*Qx`jPXGJHk<-*dKSYNC*jYKy^Y6@Mor}%Zy%|=rsMgD zVM3Kw3W^;Lc53YsEa?u3b={GE@Sj&m{(I6qPii{6HqqeL8WXIpcQ8Ar3y*^!R`1@o zztZ7$8*^$`G3d-c$6s7>b;&;-q6<8u*%)_(zJ&(r?r zd+)&1@(3pMfic{a* zu*`MQe`j_FwgaOrmm#V0zuo(@cL?yO%OtlML+PtoIhEjs^>B`xfRk}Li1 z08f`#Wb26k8SUvk&*)Eeg0Wm3WX;P2PECuv6T6^`s@?V)y+&KAy=cM|zzrs&t6zL& z#y*$Uh|WI-;7}9Hsjao}apEKws+-AaP2kXlb&RxoA1wrfFyX!+DH~0#uQJgKVsR9@ zxDV~_A{V35#yC{r0K!|~0MSvjB?NeYMe*&k9XEqiQIDc@KK;iQT?fzeZH&SuqNKPoYaGVFU_Ib$3Q2Hc zPE8C&84utvZ+DV*UQnO6!$Tlk3|!j5UxyRam|M+xWLJ{!Xm@b8mAEmR{? z26o0{;%?GiNADVai|ar%qkSnc$a=vfbg2{?o&v?ytD!wnwBa`V2(Qx==7(nW-_9?M zNB*A9c^GR0^|K?rxV8%^#3#n7q^Er5XMSD2nN zFv_zj1t@gZozuSXs2K@c^IoM|_>P%4M|9?#OBQyi;QCM3lfc@SKqYLJ4^Zby2PQ+K z>O?zpliD)K-@c;~2CaKL++Kf^>5=_2w^OX|qpDtmRYo*Ds&|YAMvH*<;O&(UnqN2R zTyZVkFH06O+qQ<1A7YD4ChHqbx4E(^F7RL87@=Y80qQ2FvEG3EL#zvhF$5e6e=p;- zT(G^Q$vxh<5}fmyI`xmt>4&&BC_83jhBs$vjJ3rq&x6V-%~}*vM+ZV4I%_{^tmvkX za}g3rWzT)P%q9;+qy=ZuTc$GA0F>9YZXV|NFde2TR7WiNPq>-khM$O^EU+wzm3AGb zwUZ&ilMkEmkryQcSdTxqv;ORF`Hz)#n_n&63Gl)N`mDy_GP0#2k;hm>iM6VV>74S> zkpWXj(MdNu!zKKP2pcnod0T&+gani4_#V_h-KU`GKy=A;wj;U&rIysATmMe7-Xz8o zehv=v%=$8}m-|B9=M@b$+GhOYuF()4fB++_+nyARKOE0}f;O>9+4%T5vK}CH@tKik zaL@InXz{SpX)~gBlLfh+(?K7YL|;2icepVf&G}mr=S44mvX-)XoB}4xk-m90QOeH5 zlyP2jU;Obt;4@{YkwD&gG*3s-Yt^dqhJ+g5Z69-t`I+U648SDK@?3@!k20AAbpPs+1E_3G2|32w`NB|O^U(lZDhzPfvwA|Pq5k-`cNMu zBe4_700%EBN#(pVC^R&D%p>2ep4YhG%zfBqTWpM@C0yl$Qjlf4(Jpg*Fjd#iQduQdN`eF*cS+Gq^XizV|OK zj<@W#x8X=TvK$vlJ|)>36g;2@53>fkKPeW#OsmTuALjEaqb?7DD0bprIw7X3B@Sqa z|0j)|JT}b09`wLh^;I-v5bPBuBHgK~GlTX!#{W4pzI~L&Bzd>rua%bzO2nBp(?K&| z$B*&l3+SU9sv|=(8!R6k(W*94)*#!`R>PjON|8h!)&_lhH-Q?x7qH$ubrjoJuT3Eb zuipM;st^vvY^twrR^~QG`ZqWry;WGt&kQevu{kn{ zbOjA0!X4)Pr?=Q9_SE+!r4*X{x7w$clZNhYT4=a>JX`Kn4|zqtO`Z3x!*D{qX?FXa zb!IkGX2{$V@sL|H5fBR~;FI2ma(>BC*Wdoa*jmILvnQkPK4EDn2GSL{<=2(BE*i)F zt`O7YkLBQW8}tljF$#5Y7+Uc{ACD8A}y z;dT6=nSRk0MZCG_?Yms&hF~y_b~KHZC|rt0FiiOlRCGea(PLI^P~{<~4*93whksKr zB-@DVG*uWm0!e3o3H`+6&J)pw&hnI>(CTskN%2;IT9h$N%G4J=`vJf-d_b5y*pl&LOiL<;k4@q zA2X$CzU9z$qO^V#3bxPAg;v&jYBu9QV@?eYioHjyI5R4QFO#I0W{3j# zFg5cE!)u@!`yRW*f#_o0h6WRY)nG=P9}ur^B}GO;wn~p|16Be9E4-^Go$+yFs7HFi z8L>98Eo>RfkywSjCESJHUrlky)w6sXRWYTl#iLM)moKueV=-S;P)qcp?DD|-=u>Qx zy?&Nchq`lzB4``<^LTgX+$vU&*Qd-MM|a4aTPp`wC4^Bq14$Sm6?GJK%DjNJid zIop6m&Bse0#U8>{_%!Qn1nvbFUC6 zj(>aHdbs#$Vz2XcZ30=gW06E zH@7B=Zgzn+n;8k4g#{5QYdu#FXun5pENiArqaOu_0&kK7xt7{>^$Z3Jb%`eZecJ6% z0qaAhAKnRa>m%?DM2)V?@jkMp#K8D&W-!U$-d|8fo;SnA8*38?Q;;IZH5h$yoUl8r zCRz%HV${zUw@t!%>^Kfcm`fkd64K{$ZZepi6OlBB4q2F9H-x}xe7z|aHAB1ldgaSt zjqvjlB)*xlJ}7DYXww>&0F0@a3h*c+IW*b{BQ%~EQ-#4Ache^7^VQD)BnUhiW}dve zu2+$&ToGA2V2E!WaHftwkW~Gw3jAzTPII0U8RtbQC_Mpw;#M&6oVqheNm63S; z?HoX^=v2~4AR|=SP5;>zv+z{KM(9&bdRX!Y>NZ4NwZDrylp3>kyXKMp^Vf=XbQ(Tz ziB#bt*;OIHMYb_>^f6&88J?!1U8$l0h4~Huy?g=G%(KVcXl&>cj&*K3Wehm#^o&XD z;<2Z-S{PAmMyj>>GXT!LJhBQCpDqi*K1o4#(cAbh3|w_mJ_f2U3ZzWTWZ~uIuj(r_ zz?HGpb-$V)EP5EuL-OhwqUi1VS62)vbcX6grYwcmn^=|NDI=(=v;3r>F}EJVQ>xuS zloFER%l7rkN84&WoIin)T|ZBAN<+#|3+dbluh}PZ$s(ZgH+P1`o0!4dpb(=S%I`0}v0( z`{{sWV@JorucPp{f#)-Grm-S^c}ZnqOv`)WV=>}{@wJ7Acbl9a%8fyw`8(o+aMlBX zvUu1zu%&R;%kQvAAjje5(iQg-KC0%9v(SZXdLdKq+r9SD>wbiFl0TOG5$JGfpG>Y2xh6bR zgMZZdiG^0oqUs4|<=k)c?(Vjk4 ziHC?Fg_9vU5ncDa>L_;P267e0*sd{6E;OHXIpybV_EP^fAQ5Dm&muzbAM5QEOV`D{ zTL-g0n>WNTXEq4WpMFF^p;rtBIvF6#*7(@2|3|Bsaoze2`~PVb5Kr;A=U~4&S&ri! z_;P2$D<)}aKOP3f(@ma^Z2$^+q|u*Npwtw0SgnZ6I8tEQ7f6AI`$s2z#Y>CgJ?tMy z>cv#_ekR%ZE~2(gcvL-u=)lG?8!@JRFh9sf9k&0mGy1KRA?M&;+=EVFHOA5y@b7}8 zj1IyGK}NQ!s!I6nm|lGOJ zQhB}xt=R%wNdOmHpxUlXQQ8WrXXX?`_Vz1DOI^LHu# zHW8Y$g1}}Z|MY4z={k)+=L*X+3@`DF6@mqus7a3wMO&}Dq1l3V2n@f=7*eeF@`NiD ziHTG3bXrJZxQI zbkPH9BAg}3S7y_1WXlnIsHsNdaq%MXS|wcivG}pc9F_ z4=gA6H$>uYhc2nFWJeJZs=_l9R{3)LTxEJA26M${SpKBz4P~h(U98UZ%lCaaH5gsT}tv)@8qcQ#a$>{>Fyef zF_%b||7*)Iw@$Y75_+1lRK3hsuH| z$hwhls{TR%}?g?^;9Y3$j#zl#hF%}C1#&+|n2Q&Xp zd1yArourkW6&XJ7VYKP>*>!{>x#!*=j||bu&IZ%X=M1)d#L*}uB+15qawK@cvgr$+ z$Eg?|9F!Gm%SFg}q7br~FsM$a~t!W*YL-=9dqU&~tG1kbC*A1w5@vcSKVnK*_T#9@hO1IK zi1YE4F8wT7mf|sb5*oW(1pdTL`Yt>Pm$mC)O_!ZhI8;Y^YC#Poj~R73AtWPFBI6(< znxy1356ZW$+aiF8dj`a&GCKS?wYc*B8p62(w%-hjw-aj)v@H&iz2F0m#r)~S^cHXO z@aJY56*kLc5f^xfp)UPe}x7CW< z`dWfLfBWy$WMk&TN-}|_^`9=X3|9`E`haRJ&sQU1JPnU02fS!=9@?crr=!2(1aZfT3A5EpR^Qeo=*8CTcg-N~kRt zriz=U8Y=?udLXYT^`OlD18eU}{CT7VfcLUF@L>y|iXr;Uh$`71B39cyJ|R*T6L;3u zv6`F*e6x)BrG#9SPVJOdFUWTbS?o%rREx^FYulC1 zYDv!il%|%bt4bBZ^;-(twOgO1p?A~Ma;+>UqDTnV%F^{RcRvA(Hl6ixsi-{U)Z-kO zN-{fsnD^n5LD>1O14F(WU%r3WNdb%Aj*yYD_~n$6z4&gR2kNSD=RRx7d7*~QK*H>o z7Asn)gmQ4PtT9o8pjNtY4&7_$fnte_q6w3MrgwQjWdRk-4t*hMCLM`5Osm|+Qujs= z;aB~-=Lv#CEs$AB9S{A8^!8>BrX87H6sNQ*Rv2kUu}iieCD?f8##tKh#*mf4va&D&N}N*?>(VYNg_PX2f{RJ8 z)Ub5W`%U+>KUOY;GvTp{TCDnm+GJD*be!Kf4)2%l4XNxxoCK#~JtW`UI&=L!!XMC< z8{P!xjY_MyB)pH7`kIA%G5lWhri)7uou|FkTRo?4t(q`d0c=m$raJ9tg2gYoS_k@jeD(2z9cy0lxHZZ{~;t5Z!o5nd>}CJ z(c*9rT8AkXo&?^j+)!S>k zoPrSE=I@@@+3=7b{&iR;l&sBIMw8o$XHS8Za{y+kw4HXBX@0&PwH2y}A5;r_68a%o@6pc>NPZP@FQx=` zXoIKyOW$bQSYOA`SNV@Lbkc$h#rShZ-qw{1$2aZ7*t3ba;q6>aUi{p)@&w+dmCTaaSf-NAe zjY>H`>=8JJ2iP3&u}%5z&aWfe%~u2GQZncbE|dB?{Ns~;TA&|3;+@=%~~f6Z2?gIswl4cURP2OPaNp2No3BKF!?RNjjdZP@ILjyv)6`!08C=!X{IM4` z3I`{y`X4WIkDu~_#K$t%YJC(8y>HWw56_Dl8sx<|=wbkM@F3ixII_E-OAZyyW$zxMX1ga%eCBN?S*Yhr$#Hlz+nCS6Vnf2w(q+kR?Mo;S+l7*e;ORXIAXFU;@5AOlTIE(M@N z;)_H@8ioCjv+p!gX;)}_1VZhy5u8lF9kl8!-s|I9I7Rxidk_j>%Mob$VkLjm*c3Jg zS3b{PlrPcH<{XV`8QVBaOTorBfIOJ?5}nh|X6=_Gb9Rve!ml z^}7QXd^i(rB#Nw`+Ca?mSnUq@Nw!M(K19lNru1T-pgv_7!OR)nbTMG|p2TeZ7&etz ztYYlw9a(4mK3Tc+UC7KX!m76ACvxqpo5twUz(w&_Ir!tc4CZE=Bfc|*+^#ww+ea|cJt=awoa9OaE2(}#KysC^TlX{ zH1bUaM2G%FtkBaxus^C?0)L;R?c46l0kHE>C4_FUGgw`GXy*B07TBeJLjfW4e?6?w2+6BkFYcNJmFCzBa?I-+RcO}zDzhzrhOZ5PPM^RXXw2S!4_OmgL>2k!oUD`dBUoUDZ%C!VO;R3I$*vy!V6=7haRV6*BT|U z)Peba+qM0*K8b0I^!qzDZk4^*6EkA@#Qo|SLuSj*Mbbp?GhhntcsU1h=p$DudJ+_{ z9L~4@8=j7J%@_4(OBSU9G_xskE6oUYmFzmda0hFbyguE54yb4n4~a&7ouJO6${Gs% zVs-VMlYuNc_V*9nN6(#JKUTyHzQlR7OytUw2h6Ju@N2i3sX{^3l__s5ZY80YS23kcLjypYUO8TW0#Kf2`~KT zj^TJFovWHgRw5|JTZzg%s*$fBP33cnSmt=MXuAVGBHj?TyS|ov@gs3S#Z4781SU?o zkXuMuoCNLP#&D{ne7)oBfLx_Jf#L%tHigdTpUuRj+-lo=x`HMFVh{O=qe~WgYZeAZz3QX}90y?{}CaHv4JH zM@5rq`lj^(XKsptQathR7o^jt)-IwxDAQ;NpQJAh6=uws!CQ~84kQ(qPLkO5R&b4Y z==DO%rQP;|R{Ky)L)nJM(YLLg;A0THtZ;Q6nmuwc6s1)0(YDq9RboJ0Uh^93Z&fg5 zQ}VqTj~Jegs|yxRc`F;E*zB|tb{PJgQ`_Oe;_8EPajNeVnkfbzTki$C)1~nH8^Yc z5T7+*(gyF=bXc-?a(bn0`w$r@OxY%Lu7l6%tYsuWz3p!O(e8oF#RZ2gCN1hBn+Psu zK1r)cwKjBn3~f1R^69c+kr2mK9jN&trE>pryC*{mhx&-_zkgt1hrO5vr$eQ7@aWad z75gtK^4FGSnV~@MUxu-obYXsacRMEyPQv>j-kD(3feg~l2#7*AEeaXn3!qFZRIGY+ zbd5Q&A}_i7lOj{@{Bm>O3obdJEE}OYW1*7hOyHs5DA8FGPkrwwZc#fhaLi5J5LDcY zs!X^S%f+G=1c%ehpe;~iMIV&&PK z%`=xsdCDd6H;HWegTLR63GLR>?7>6Vl%`XHnSo!=rk?V;kShc5b=FjS*keh~5LNYt zQng!n!Hkt+0f~$cBQS5+!N~b}suaw@_~dkI(wXjIc5YV{#L$@MMZXJ1(#pDiK&1dD zc^sxfy1`aqV|lV!$paiYwEGJPuojyB0t z)J2)%y8O--+{@s0d5VsLTJx{8Ag~_9HdT3el4L0bDZo9qlCQNz$DG{95a#4l9;+sm zHKv5HVDh!_mbHrZnHnwh9$Mgvh`XPErQMr+qL=VjP#=kQn>F$f5^j@ALX8uj%#t%)1c+NWFwynyMg?AGlwgO%L9zL)Rb?0RG19l0ql*9q?Iy_>ZDXOUOI_U8 zWB>hup@QqW{mNv10sjJXxgIUm4m?X?GQnQVfEQLwQv4-sRp~;}&O`8oxt?a_x^&f* z{f5a*1@(6eZ~^$N_lvj9;GfL)yi5C!sRNh}O;m7e=>D^K1n$LmIgYXEk1Qe`U*D37 zjCD*psdpkWculG;o4ErXISKxJ|CMB3b1pZhHH7p>B>g;Bx!G;HMvoUgInS^co^=mB zzgITbbgZ2Una2h$wWqE}US%TH77=f`<$HIMXAO7H>294G2oz{@W=SRrO19C8G1hc& z#=S}~Oal!Cybk~Z1a-y>h&x(~5}sBOBwYBdTnjjpAl#ki57ZUoWtv*DFC>{1m4T-b zdiiDR{mvh!4}+(hk3Qd(l)?af@R~PAdmCOLwuZu?Y=@wOVG|SF(}}aG=rI3-nxVAi}$!EMX8A%Ibt-Xj%O02JNuZdHBf|YPMfQIbT$>gf`D^?uW(fXW-CUe zoF@75G0$2ZC}QF#C?kq^Xut?`y3hCQoO3{}SYZEjSoub0!0oJ97ee_QA$=H zvx1{gzyM_HQHAZpy(bmAgSFD!qG80mRXM@4fxk*#Mx%QeMG}HnvR}}S zDcOYE|JLf6Zm17o9+V>v()$=G8 ztSqL8b|3@E>o5?dawzi4Frlg+YKa^!3Y74HjtxP7(X7uTju(kHlS_^YA~Ov!FDYB7H2SXz#&VM*Xnu5))9+I8HcGPEmR-}Vcq zUV~TPc}8k$atCJmvsQIiD}1-jEuM5qN`~hf^B=P|sH0QPozBn*NIqGso_V)<^&6*V zvU~pW&EZ^#MY;|cn12s*Qt>p;o; zB(NZmA|yD!tNnE+_Pp3_9O2!%$SUpf?0>ALbg{wz1xyYzJky_nOukIra}A#zlJ4gz zV7}z}2jBGO@eZ-k-hTPgdC-W1kO2=!wEWbR-=i@M{1@E3h?kvM80vq!b9ZIH$(D+y zNFkSbQ>A8+fT1g3A!+bQKMUb3JBeo6Xn!L+!$`PPu}`a+#rJ-x76L}ZfdM9`{tq@ zF4aGF=+N>WYDs#c@D%?aar_R9?b{k-X1E+>`8wp;ELSkE%xz@su!}q%GOqWf_rPWG zAZupl_(8c$VwHL5)K8!lo4A6~vU8V{Y?)qYEPa)~<0Cq?ji>SE&zt0#JeK@iNxgQT zyu$h~wG_+tzfrl1Og0PDC+c3&OJebq(wl&M5KcZnem$hQ6`d3Di)p=?hVfYkp#)Ft zJDI~bWmT)W>wMh(qy8hEKggn=G{W+g8LrxdtpaoUG+q{1p}#^WLF)QA!9-LZQB*3M zq?eA~FY7A{FzdGqOdUc-D+fPuvL1X_a?>|O%&>4w)H_kOf*bz}&?8qWBIM2fOzmHD z|L*3{=ipRaw-;uEhM3di{@e-+nbUr>;}9MPptgSTMu#N8F<4T@!@A^N`=gbGU$PI0 zUdB}_H`jzAX_Ev>#R=OVjdsu%jnAM~4D1|~th;Y!vovYigXl)eh0pN)zF;4VJrjvqqO>T|LYiOFD$(?#QC<~20c zNO0Md>Y1>4`6&A=uicgC991+L5LAGUTL`nsLSCra^h$>`}TjHEDi= zef^gRy|r6Gmc7o|zAJ|PV-fwd{R~oI;K;*IM4fjrU`y%cKJoF{Z=R=p@~@jfG%;W3 zHk-uEzV;N2`gb7%11|l;@XpR(ru&mGjE2)Q!w{EgAoBH5p!20-H;iUw#HqvX#Jq0% zWq4sd2@48s6lRg7#QRa=-r9?@D~d~_e+cRj35|9eOJ&W+c&U4QY;V1+31O&tv()j7 z`EN6&pDKMNy5ZczRgwlVy1Z%fmW3Y80R%QTA2eDC%pX5!OVcd;a8EYtJVW2Tn5&m) zR%rHOd$HpVt#XV?$O{ ze&t(Z#>P$pAAQ1Ezmj@2w8_2l#wW?B!OF#DfPe?`( zZnW>#hWqPlV649A9$)DqY@uh8j(Um!E2ww(ZI`E-^ibX#>hEc?rF^(M8cek1uY+1D z^JZ!)E++kD%RrAo>R}jdXog|jIa(KM1w&oIzh`0+&>eA-C7 z@~pw%W%ATl(;Bf$`A_?4`tX?R4OG`n zVDgCto-uqm*jDBhhoe>g%f&zu+pp46Qns3`=C5e}nX=iKj~slcP)+iLMXJ583)Xk> zxk~b5mW5uQJwa~%pM`tjoYh==15auP)i^@QjY2(@4UhHmg=~b(9bXlZ zNVA}k$oh*N(m1ObzaH^E1(*`GWA_gLXr9g1enHequF^4+soVZYf~3~I3iMk@_N1qB zDMz_VcqMrDdL!nHrBCji5>c8`sa*=?0c1Ra)3 zhb>Q}h;f4W&2yr%5m?D=3^JLCgDYu==*M-6cJnBk;#F}hMolLOiUy*v`vUWVTbHL6 zXV~3K5`kXS2IN@ixRv_Uy@`K@pN7ZaAe9oB>ol>>y>wy!A7$)kp8Ccb-AFQX5Ikk*BuT> z0=tHv*8 znH~RgY2!TbBdcy34T=}S2X6K&3_nd7TYbDZ{0(uX^k9^`XTgrKd2D=+jMhph9}&LU z+MSs5@6CK>?Va>HGn=Tr-t36ueMY}Qsg^gQ8;_*Al>GN2sHRi7(k8pZtYq<=*UDXP zK*6}E=g`0TiNE}S$vepAkJqo_50P&1MqwK3YHec!NQ!?vq_h9JkNKyVT|;E~u@O4| zX8Ak0P9;(kLki;qA}Rk4b?+TR6y#U5$HguSb=$VX|4v+Zio*MH{?XY%_tr*5so z)sRxZtr@JCdFXPia;lYruKZ{WSGSHYy-vk_(eD3EH27JtP5qtE7UU`kF3U~#%TeBD z>jeA8^kHFCA_F|3I`icm%}?XPMsIOCl|twB{ki;l6#3ikhGBoK6ptz#*|`I2raY2Pk4lQfLDj7OzF4is1p9yQe2LM{U~0iLiK_@s>*&3} z^Onls4WU_BS#oW)=TFY(*JXh1%iufvvR^7%KWG%6vW5z=a~xh3dU3RUuZztZHO$M3 z`cHK+{hv**I(YO4zYhs}kwfx4T{-=7o|8vEqG`HCe`{ioxG~g0tAo7^s+q&Ewiw&= zpY-o{z@e!hymr%x|K)#Y_L)_?u4Xm5+%|Sxs>o~Wz%9f!=$aYz8Em@G zKELUmKY+JZhTC-#H*%FLojDop)^#N$OFm*?3UxYg%Vg2dP*dRQ=~vtMVbdV!_x!)f zxsAHr_@4@TM_Ty9n@Ao1Kh}2|(pe&P)&T}zdyg4y9N}Msc2!qU1N+=d%J%xcjVs*u z5kVMAY;2x}b3p;5&FS6iS+eqGGq&`j14ZFr*zFfzg3;Ge;GpKX@wqZ_MqyL5RsVr2GSa+DADZz_gN9>gSb<6$BW05w{{b-9nY@CEmSjhY^#p>T2&AOMP9z$0hyqRX?ygt7s&cI;9oq-0rf(RH;*LF%!V?)WDG zWC91DxwrYCL`xMXzHwf#qI{U&63oA_bAJVKua)CydTfOwV^Zjv^<0vk&a!cAd2ik7 z^sY0rR)70QuVL=l;dNX!)-vcDq-V_%&D+}9hHqQ+(-cnp9=lC83O_&uLw;DKhG2_= zSTmeDARh62nBq4rvewehDE{66i;w=A}`q10*6_%PAD z_M*6f6_)9#PSb=?6#nicwI)SeFf;ZKmjV{g8?{##{rH!A6$3TpO4FY~vKX_>T11I% zI}R#L>=P>8Ib7d&U9{8{+HPs-vVS4c-py-4aGy?;*~`i>i^6{A8j2XJE_AoEP{7KL zHFI@d?b~R56KLq}ND`PMu8I1OX#J0 z{^gUS@t^%H0rJTxBc0Q*CCeu&u(#0!ZuBX+GoTb@xbXc`Ey1cKk?sU$G_FPb)Qu`v>tTF%Yu{spG|swN?@nmOGH&kkF+ zCN!fal;A%aJVn@l_=$R~0t`bX%{&P$rxLyIpwL;MSJ1nM7`X@q7aoa|hiU9j@ilHg1=^D72=1f} zxoLmA+&D`zjb5KR<`K^NqhxRLJBm#%v^1G5Td86lT>%rA3R^E zVTpx!n$k|q8YS6#GHpwx=vkw(j&Lm%I$}raiolDD5;`rcHkI(<;1j_1fugfd__={j z486nmhcmRsoq9FzKGNGYpY6FN8f_H)7^^$+q+Cpj6=3UIQcTLIzF<85F{Yz0GYtJy zeR#&G5xD16mSrt|?2b2F2|526kn%ydZ*lYoib7jTOYenO6@KGFq{+$|%RK9w{w0>@ z6y~fSmIgR)L|m3H{Fx8=NM{wcrtI=&daUo3dTPYq=4`p9FDMKm*lrez{kc9)d1Wi} z1oP)Uoq?7@b*t+`0#SzKE*fg9xsr^dxZKhz#``Ux8w}zyj1NP6-8|piF6Z7)PpVtma8&biG1x{zZ{ud#;{pDS9iySDk9p!o zwwZNtQBB7~ku-^dVL--*V%PyX-QS61neY`w6Z}Y@+#9 zC5tf=kj?wYQUd&h@Q2Aqy0~xfXVJ(f2BRfqg!G985UDIY{qS$HioYq0*`jieA$&>&LBK;AEoYW4;R!@20?W$YbT zbPn3O#k-_KXD7(kGE^{IJZb~=r{L`z&{&5m`t|WOFOjgvxNTC$Lj-y?!#sBSM|8s! z93gnBYL2B^tIT0%YOtA);xqiKig^F7tMlp%p)qpcA|lA1i?oM_aPiOlWZT=bH6Tvn zU^Axo;MaKe@l|?0jMKE-S`5bDBD*P>@CeMv`OW`mmlo#h25y!2l?mS4ieS^$dHIHM;X^c)nu?X^+2Z}=~}Mn_q1FMPccGV9B-I-mK;L@4L# zI%KQ#p!L-;$8eMqee>IXAq8TVwNDZvdiO`L*3^o5p5N`X=zQ;rzZRd^wC0wtQg3sOeLiC$=s#fk|+OMhy? z=hKx4)5VIUz=Uj$Z#-q6MP;OOr?g~vQXSztqAdkZ>2;FITQ9f1FJA@1T;)p~i@e_) zg-pNMC|%ZG`6$WU1B!Rcfmo(=*V`!B$xsa!*_9e?LPE-8K1J;jn;*NkHcC9!8y=*m zGV}MAL~)4#DwuQ@ecF`$yZ3WRy)<@{8S4Ge+0Iu$e=cqw+fGmqhGDflr$ws5d{Xu= zTkyPzfcy|B_j}L+c)eFK)suY7u|C@SNIc@{MCh$lStVHqS-BE7sPaAa#~llE$!4E? z{v4rKJARq-T9HOM3Rb-1iBxKMv}(Gh8N?;gX}WlSOXKOuje<6!k8K=Iv}eYKJJ@D--&d3)OA!#Da-MlS}KJ} z%|c7%&W^Y4B)-JfH>g{6GkC>r zBj>}2rX+nOdPzbOZT; zrJ3yS`54vB9Q@^PlULnW4iku=Yg6p`KL2Z>8^;Zph++uZ<>l}qSKD==vK9=M&gWR7 zsY!lI+ExD7^1@9xPeAlYTXqUB9ywb50hNK=j$82Sk5I0Y1oA(sGc6jPoqGCnDxytS z;V|75!q74-pv3U_t$ptQ*T;EyHMQ<(SVRSs-m7>N0V#qapj7F-2+{&bl@39A4;>L{ zLZ}7=MLLlZsu1Z)uL(s6U0R6L03pf5bMBg%J8Rwf2lifj?egvKdEe(JBw9oUc+NLs zFVF+L^6kIA6Ke4b{5Glcb3 z=UUnRfMiPV)a}Fu)HYSeUAMtcu1jPAarXe2Dy+e4v2>3u6zb$&tNsfMX=E2qhk>6E zHpqH3Y}xMXR+-+ka?`Y1xA4|;rmW|tm%mWk0V-$xb2LX#9;kkE@=?XvD!I_hgHC-1 zByz@)Xh%4;E}qWBd<&O(5MffFN0^9zab|U#aakb~2JUFD0dhHIqdZ1|=zCq%RDlcF z@lV>M5ej>ih1+K=8Rn!PtgiIGOd4cl)wozS?{}(i>fdhU|K7Ztn|l-_&_zxzzqt~n z(_^9)7&hg)ll(>qQ3yNg+&mTZbI-03iRpSF^Ux`j=JUx4r!!s09XnG18YLQ4%6~Ph znyk1(pi^FlN>}2$-!Ge(Nj>d#ldz@Ylt%+!;zd2jdYf12u6~<5w*@iQJabP1^lOt( z4Ai{?s}?=eIXik?vG&zau*qB(*=OZ_bcIH4)IIwN)Mb*`CvZ1Y=WB%79DDM$T10D4 z+7b2+_CnK&b3|vAP#E>e7mR7fLJ<>rck$l|Z%o~X!<2C!MPA(nHd{Npcy4TxhJt8& zS-bzPt{$yB>0rzjNGW!>5Ne#BMOX_I;2`@r?wwp?q+aFmNEA{Xfujnqt{>LCK1dI* z1T_vy6SGb|+RJb2o%D$0><@3aZSxLqgv>uW14QRW9EuPR^Y!~xcN5t6 zmth)}@z|YNFPPKBPZaHfgRk2ifl!DDG+Vgyg8hX`|Hz+2xB^xb+i33dHIzfniMe{% z(Y3dewe9zok`$FLr{V8(=~IG}o~xDWyZo8VMaR3|c$KM0hz8F_FMi^etLyXVVFL1Yuaj?|b$B0hOFsEL;Ryv#<Rf;d(WH-_-asCLZ3N7V_%n8V1Xk)^niHgKWM~zN{gj>x5907u5UR?Ai(injbr=djMg>CP0v|i z3&sU&#uD!#=1{7ytPeDudPr#;Qhh_|zG|E>U)a$tzCDGc82GXK)yHhJb%obT?YP_I z96abak9I|SF>_0u8pYxh5XUsBuz)}L+F|+;EDz$J$oRQhQHp(X=Xg`_5)$yyh4Ai1 z;DZ21vM%=udva+WD5T>OH?tbQkQLUM(rjHr?`aMS02?d|7o~eq_P!+Zik$pZ&IkIRx& zYMSSL#XksmPHgigW#vpqSJ|_;9~eEuF!5?Gx?*hXItQKt&e5Hq5L~8i|LNtbNw3xO zMXs5SAxn+o=Z&dCG)l57H;`pN$fyI`6+!)($thvWH-+Y#W*Aw-_71ch@d{2p5B8>4 z+6o2K{Mb9&^0RAG^HQ?J>Se`jVo;=PnpbREQYjFd9XMT4QR#4STP!uU-?|427XR_E9z%u=_5{Q%+9|w#-cpga>$~MHVCpujS z4z}fcV+u&kFbJo!Qyhz#n29_{Sy!bE39SQO^J8aE+Rz1oxlVWE=6@3!#+fYg>!8kh zMYQljwtAHy6RvyHbSB`@Sj^;u%!I=ft?uiz=QzN=G_0Zfke1BMAV+Uvx4sseUv-=z zz+lC7@`r|MM7J_mYh3?3OIEuv+fAFTEILMZaU(5Imy1LmVY6W8lUo(-yG@#KF(Gj*IN zhSx8M9Zp0|+i&P?kVtXt`Ts+0vd)>RCHJI)XZ{g^c&>VBPPY}7R}*!_r&noF!Zc1; zE7B|=ZjPh-O+}TwjdUw3!N{VNp}BxP2#qp#FSE0E1Y+brghn!TfwaA} z+SYI2zj@83w5;9;_-(}K86>+oAvtF%IC1X|nOoGP>G}hnXhUE_-h$0=@DP4Eokt`3 zZ-%#4UxFF<9sWTH!}CRQVU-$gT}D%I0!Oo+d?$$y<`oeR9rG3)R(M4oj>bAY4L(;T zSi*4C#5ocFnkblNsyKQy+43V;(u=DIgHgDuOuBy3a#z9ebfIiZVyZ&+4m2c_@@N07 z5{l!ansH;a#4Zp(Qs%oPMXukirdFVelMFKHRbf$AV+t2X|c*5;241_L8@* zN3`AO!X~TxSAh>jHdN#7_!0MibCjg`>be)dqIcT-`dp-t;`W@o3&?mU5c$&lUYutV z#6ZqfH(y|X!vuj3qxAr{iG|K3M6wQP$j}AQ{VFTymg-X&%`)fqwb^krSa;`rbws3r zbPJYU7oS71?1peZk(DQR#S7u{)D*#JtptR!Fncs z=$SKsKK#&QZxfr9%wf3x2go=~ZFzPNfVW4_by=#D1x3p4+=nVL$R;3uiZgGC6IJFq zy=@L2su0k*oQDZ?NvIc*7lsDjRZ%N0%{%3}-v1aW@?V63$_zWm{rVX3H!;^a&DWe= z4_s)j*J5QAEBAOMUr_NYO58N?i3s89J-E#5Pw;n-YN@#jWJ26%R2ZUFp`i5%GUS-0 zc|_4m!}&YQmf>n3MTw|1<*(zOEFwW<&HO#)rFdxD^0`K^gx;AOfd;j%Plpm4`Mu@} zLk!7@*aHH-?7o6*amRMO%j33$H^b2?wz_pim{Hp zqY}>BBsPN@O&!O}i_5f?Z~JaYm`%4StOgG3yYYCHZ+ht(@1r`qHM(BFYG%h`OZz6%SI5b#?w`R?B zUW|swy%qz#ZaA&?8*p8Ro*!^!L0wK47f}}QO$*aAry$RFA3e73@D3H7|Lu>IKPsy- zmh;|?rP9}N33u6i#)GRF>6V!DMB?R$r10=p=c6~)6@~M@Y4Ar0>xGNYEDGG*KtX9n z(V{5^{gX4)!=7T-uuerLZL_(~#)MJoIGrosuQMYIDnNq8Ra9}Z@^aOLmmgE%AfsHZ z8W5miyVdT)L#8Qc=E)>dVdQ8YzEk%C{Y`QGY!(M~`?3@GviX(qOJVmeM}`~Rlf)oR zAg}UF9FrQIrA{S7^@Zprm*xHT))&rfXZO~Ht3mRK=n0Z);t-zgBcV>F8|w3EA=Pn< zn5;3pSEWK9vSP49R54o7xWLaZ_8D00WMj@cPR!DaBBWS@y3|Py=9KhwqG?Z~h)=&8 zXYb#zrknYK24iQI!8%R9>tz)eHRAnQnqAYyOr2@*fgcN^Iwff|IEhNRu_iaA+Wp;C zYi|(fnMKT?@0bo2`wIrHc$*BX1JJC^3I9ZrK+Vy8TBor3cPw8%y#Ac-%Kb9?BV;y6 zfMH^cWUDyZc?kO`^ir4)dz1@xpC5_)H5}V6R**FY6HDVaY~^#bFwtZYX1kA%-aY^+ zz9GSk`Z4lJH`Y8+nj=p^%&RY6ipJq6$?p!%E5nNNE|iDFI+;!R!6i`@!s!b3jj6%e zVU~@-S;ZEfK_4`9ci6S%*TZ=72MNt)NMP3#i;3w;>%F1<8adjovO{Ds_7h`|)2-?M z7DQz8juYHT0B6t@}gx@gy@bCqK$;mIg zFRI>xg00FKX#7&;V)o3{>QX8;QWe$I9+%GM5(6LzNq@p^D@9^UDbB zl>M_1nYJKpVViQ42G-_P?O>M;mX2iC>=0S@d~~O#h!p#L{e>{l{N(999sGfuqB~CK zbff6A@ ze4Hbf5h0Bke9u~J_WpZ%9O`^>f>w1pA{trb$hbe&<(=~z|FMskQO|j#&`(U5N6KtHL1c6?h4M3q` z(?^69;Ov;`lGgh-xF-;=an7pQPV9G~Mvpv~Voj7m7jUZT3G8PHr_5Hn_flA`4o0&j z5n1-It*w??cq4+br1J5%TRIKD2yOuszc9NY(xX@%96_nT=xj2k(7MX~*Q`vYURch7 zGxVx(mHo}3_!xc%&peK2r~v5`Pt2M(5I?A_9jpZfg^26#b(Y%noD@;0L!ZrlNQA@= zAK0UvKc9?$2o$PzMT2(FsNwK7{IIe;KBcI~^vIIOU5VivHme~)g~W&I^xp)}QGEUU zS0Fg9?%=JrWk3ddNn`Hn)bCgX2@=Wsf5k=%W5%)x&*{eq2qj4p3=vLkkV>Bi^&R#&m=1(CX%0(W`L!< znCzFnJr@zfXgi*g?n`%0aHzT0 zXT&mS8G7ko2uA8ldOv7b_YOQ%^=MUocce#qTpbOp1D<56eQr4&*JEoE`7B@ zKt)J2*2%`5)n01N7HMe>7buI^dD!Bkm6&d8rZ)mNN(($9_Ud$#5(8D+#6GF+Kse6& zXHKVCoQ}OHIu$~jqS5xLHconvu%%8VRW(@Od*w!sc6W+CxkWy5utDm~i_*-l{CJcg zAJyKZ*c$PQ1NOoDPM#dvIqT%XEeDX}YPf=tI}vOSNAKuYG?)doKRPK>$U_LbNrwQ6 zAC70(O+K~3(O1Ex+Ysuw2*vGr_J8rCy?^)-Vyjx%jqVaG;GqbwMTgkxHjze#*V;}J zOM39_FFMX)be|An#6vt9s@6xa`un2_S~(x5XSv!?!%r#zX~*O}_I-IRdBofDd#lWY zou(=0gsHi$&{_&O0@En?5uWd#P;mtyB3~1z6$CeIsIbslWz}L;_YtTMwDt1`>??iD zL-)W(x4$mD-mA-g58B4%9w>1|tohDT&~#k-x01gvGKNDOo;u791t?}9Hu&Pv2E;;cI5YxH z_v2W2{;<9Y-f>=DVDsPyKc%vDx)1=8TtE}}RLhiUfwIS{{Ow>)?!9Db#-G1;^ue;9 zUkksHq<4LuoR+&zy(B@dQDoV+>$r6k>Z(IPOXdvho;%vnMW9i$>uDlVrsQr?b#K~` z%`&vvCDT6*vQ)zj!VEuH0j8yW!xb}#QK1DEmaTdcjU_H2>1!7ft}SnZdD;agoun2o z_WKJ3opO#2`Z0y35%TMm8(2D-afQeI33HQeOSWSL%n$jfMRx12autM89mXuuz)ff* zmQV5&C0$`2G=~28Rm^G)oF`{5Lv zny+vkT?uNf>b)A>*zD-4IDNc@c6bWQLcARvodB7{Si^^bd$7{I3*)R-7sy9g?#I=$ znVN4TnZuTB<2a|GO2xDJb3o|G1f75wc#9qWCOo)mLH|~5*h^7fdEf0;*Y$1gBcugJ zU}En5r!Um|&f7$$LP$HP()A+K9DXqht-v26Mq^J%`nkfD z^9Sk6;$Cl^nkIAq%DdMf#W}&nQ6>o`sSq7-tzh70kV&{yqC!}ro^qlz9QOZh=c6n-A#>1!5A0FX!w)*u$ggO|mK}OYi0#aYhOH?9! zH}=B?-%~K16TZRTOeCUuUJEsjJ>zcqy6B=3H;F4hb~rynq+i|8WQqagtGs&BqpD3P z*CX*iA1xkd`p*m$WKlQl7zre6u<$+rzdmXf5x!p8h)o^2cvs0b`p)=8YLAR5@>`dO zy;ES_##g27&G4)=0i3N{+ZAE+jVXRs)3Y&CT29*u-zM){T3k`%Aw}P@%Ve-0HesOp z^t^zx!-Ya{55`IdXI$wrEi0`d!gG#aRYrUDS`zDQ^`d*aC$??E=;w5^OqnHiZzml~ z>)>1qGUgOG{bcH!!4ejQm>oFdJ`%`p$8ntjNQ}NnUQ6}zeY|i2&;sFXYm9?V+`T^x zuvLSR`wr(D!MSQIToZS}P@w^e8*1k~y|&^?+3OJoM5y6@+6{$pqFe+KauD$OwEmd5 zcPik+folTv8)zEgNj>%HM)>0SJyNARvjwE!CSgpFzPEUubFGoOtb?Zo=3R7o|e384mXYTt-Yh6 zo;;(r8F^OD&}KZglDHBmx`a)a@=hr!L_vl9$V{@z=>5lJJGvY6H3kI(R5u6@Jw#b- zyYjR=Z(11kU)jXDW+bghwif(!Ki0jw=-f5E{2Sc1>VLS@wMhS$Hh`c8hx+_A`gNoC zZ!aM1f`K-2BE?2i6x4B$UljI74zLwdIGi7pefrKthJ`TVUQkRfd^7EF&%J8l=Mn^h zlI1@zse>P1qL-5jPBj&A=){44&to-KtHX%)N1B&M@fJ%;c5!zp%S-Tf4?ZlcZOR(h z#J|YfTv5ug{~#+~VHgk(Qodl$vVv1frWB?%oP$p;+~!as#~C=1 zRuFB7B$;vZYW;>}*BAqy@Z|yOnr&_}x)vW_D)Y-td3BqjU0$0~@()rfAbW&f(u8!q zklJUUjsKjr9k&2Uy7|2CT5Q)sOzq6bk@L)@@5L~I!nxGPwQ~lyb#N{Lox>dkl5sPK zsaH0>ZZ@nAZs$}Cm~~;d*ay1LL^8k>`0&bIB2YTv0fDr64`%F?pt?`lg%pg-T z)k=@rejM$?n-i5_xH-Bh?5kMNddm$S-NB>8sW#+eZHvC}m62 diff --git a/README.md b/README.md index e4827d7..00cf601 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # EasyDrop -![Version](https://img.shields.io/badge/version-0.3.0–SNAPSHOT-orange) +![Version](https://img.shields.io/badge/version-1.0.0-orange) ![Pipeline](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/badges/main/pipeline.svg) [![Coverage](https://sonar.fbe-adswen.rwu.de/api/project_badges/measure?project=de.rwu%3Aeasydrop&metric=coverage&token=sqb_2fe80aed361468170aaef32a0ff96d596456cdd1)](https://sonar.fbe-adswen.rwu.de/dashboard?id=de.rwu%3Aeasydrop) ![JAMANN](https://img.shields.io/badge/Auszahlung-Letzte%20Woche-brightgreen) @@ -41,10 +41,6 @@ To run it on a fixed schedule, install a cron job, e.g. */30 * * * * cd /home/user/easydrop && docker-compose up -d ``` -## Roadmap 🏁 - -- Access transaction log - ## Contributing 👷‍♂️👷‍♀️ Contribution guidelines are available in the [project wiki](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/wikis/Richtlinien/Development) diff --git a/docker-compose.yml b/docker-compose.yml index e9048c1..e424c90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: build: context: . dockerfile: Dockerfile - image: easydrop:0.3.0-SNAPSHOT + image: easydrop:1.0.0 volumes: - ./config:/config - ./persistence.db:/persistence.db diff --git a/pom.xml b/pom.xml index 6d1d8ff..3549a32 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ de.rwu easydrop jar - 0.3.0-SNAPSHOT + 1.0.0 EasyDrop http://maven.apache.org