diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c330e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,156 @@ +#ignored project files +src/main/kotlin/Main.kt +src/main/kotlin/TestJmeter.kt +src/main/kotlin/TestJava.java +src/main/resources/ +# Default ignored files +/shelf/ +/workspace.xml +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Kotlin template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + diff --git a/README.md b/README.md index c28522f..683213f 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# Discovery Client SDK Integration \ No newline at end of file +# Discovery Client SDK Integration + +Discovery client SDK helps in integration of following APIs: +### [1. Search API](SEARCH.md) +### [2. Autosuggest API](SUGGEST.md) +### [3. SEO Widget API](SEO_WIDGET.md) +### [4. Recommendations and Pathways API](REC_PATHWAYS.md) + + +For more information on the underlying API call and the associated parameters, please refer to the related: + +- [Bloomreach Search and Merchandising APIs page](https://documentation.bloomreach.com/discovery/reference/bloomreach-search-and-merchandising-apis). +- [Recommendations and Pathways API v2](https://documentation.bloomreach.com/discovery/reference/recommendations-and-pathways-apis). +- [Thematic Pages API](https://documentation.bloomreach.com/discovery/reference/thematic-pages-api). +- [SEO WIDGET API](https://documentation.bloomreach.com/discovery/reference/widget-api). + + + + + diff --git a/REC_PATHWAYS.md b/REC_PATHWAYS.md new file mode 100644 index 0000000..b5dc327 --- /dev/null +++ b/REC_PATHWAYS.md @@ -0,0 +1,177 @@ +[GO BACK](README.md) + + +# Recommendations and Pathways API Integration + +For more information on the format of the JSON String, please refer to the [Recommendations and Pathways API v2](https://documentation.bloomreach.com/discovery/reference/recommendations-and-pathways-apis) page. + +### Initializing the BrRecsAndPathwaysApi object: + +``` +val recsAndPathwaysApi = BrRecsAndPathwaysApi.Builder() +.accountId("") + .uuid("") + .visitorType(VisitorType.NEW_USER) + .domainKey("") + .authKey("AUTH_KEY") + .userId("USER_ID") + .environment(Env.STAGE) + .build() + +``` +| Parameter | Description | +| ------------- |--------------------------------------------------------------------------------------------| +| accountId | Account ID provided by Bloomreach | +| uuid | A 13 digit random number | +| visitorType | ENUM type for New User or returning user. | +| domainKey | Your site domain's ID, which is provided by Bloomreach. | +| authKey | The Bloomreach-provided authentication key | +| userId | The Bloomreach-provided authentication key | +| environment | ENUM to specify APIs to be pointed to which environment. STAGE or PROD. Defaulted to STAGE | +| baseUrl | The base url of the API env if its different the default| +| connectionTimeOut | Connection timeout in milliseconds | +| maxTotalConnections | Max total connections | +| responseTimeout | Connection timeout for getting a response | + + +### Item-based Recommendation Widget + +Create the object of WidgetRequest for the request parameter to be passed to the Item-based Recommendation Widget API with different types of fields supported. + +``` + val widgetRequest = WidgetRequest() + .itemIds("") + .contextId("") + .url("example.com") + +//Call the itemBasedRecommendationWidgetApi method and pass the request object. + + val response = recsAndPathwaysApi?.itemBasedRecommendationWidgetApi("", widgetRequest) + if(response is RecsAndPathwaysResponse) { + //gets required response in response object of type RecsAndPathwaysResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } +``` + +Supported parameters for creating WidgetRequest object + +| Parameter | Method calls | +|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| item_ids | .itemIds(“1234”)

.itemIds(listOf(“1234”, “98765”)) | +| url | .url() | +| context_id | .contextId(“test”) | +| fields| .fields(“f1,f2”)

.fields(listOf(“f1”, “f2”)) | +| filter | .filter(“”)

.filter("price", 10.. 100, true)

.filter("fabric", listOf("cotton", "linen"), Operator.AND) | +| rows | .rows() | +| user_id | .userId() | +| viewId | .viewId() | + +### Category-based Widget API + +Create the object of WidgetRequest for the request parameter to be passed to the Category-based Widget API with different types of fields supported. + +``` +val widgetRequest = WidgetRequest() + .catId("1234") + .url("example.com") + +Call the categoryBasedWidgetApi method and pass the request object. + + val response = recsAndPathwaysApi?.categoryBasedWidgetApi("", widgetRequest) + if(response is RecsAndPathwaysResponse) { + //gets required response in response object of type RecsAndPathwaysResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } +``` +Supported parameters for creating WidgetRequest object + +| Parameter | Method calls | +|------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| cat_id | .catId(“1234”) | +| url | .url() | +| facet | .facet(true) | +| filter_facet| .filterFacet("color:\"red\"")

.filterFacet("color", "red")

.filterFacet("color", listOf("red", "blue"), Operator.OR) | +| filter | .filter(“”)

.filter("price", 10.. 100, true)

.filter("fabric", listOf("cotton", "linen"), Operator.AND) | +| fields | .fields(“f1,f2”)

.fields(listOf(“f1”, “f2”)) | +| rows | .rows() | +| start | .start() | +| user_id | .userId() | +| viewId | .viewId() | + +### Keyword-based Widget API + +Create the object of WidgetRequest for the request parameter to be passed to the Keyword-based Widget API with different types of fields supported. + +``` +val widgetRequest = WidgetRequest() + .query("cap") + .url("example.com") + + val response = recsAndPathwaysApi?.keywordBasedWidgetApi("", widgetRequest) + if(response is RecsAndPathwaysResponse) { + //gets required response in response object of type RecsAndPathwaysResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } + +``` + +| Parameter | Method calls | +|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| query | .query(“cap”) | +Note: Other parameters same as Category Widget API + +### Personalization-based Widget API + +Create the object of WidgetRequest for the request parameter to be passed to the Personalization-based Widget API with different types of fields supported. + +``` +val widgetRequest = WidgetRequest() + .query("cap") + .userId(“u123”) +.url("example.com") + + val response = recsAndPathwaysApi?.personalizationBasedWidgetApi("", widgetRequest) + if(response is RecsAndPathwaysResponse) { + //gets required response in response object of type RecsAndPathwaysResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + + } + + +``` + +| Parameter | Method calls | +|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| user_id | .userId() | +| query | .query(“cap”) | +Note: Other parameters same as Item-based Widget API + +### Global Recommendation Widget API + +Create the object of WidgetRequest for the request parameter to be passed to the Global Recommendation Widget API +with different types of fields supported. + +``` +val widgetRequest = WidgetRequest() + + val response = recsAndPathwaysApi?.globalRecommendationWidgetApi("", widgetRequest) + if(response is RecsAndPathwaysResponse) { + //gets required response in response object of type RecsAndPathwaysResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + + } + +``` +Note: Other parameters same as Item-based Widget API + + diff --git a/SEARCH.md b/SEARCH.md new file mode 100644 index 0000000..f16b6bf --- /dev/null +++ b/SEARCH.md @@ -0,0 +1,237 @@ +[GO BACK](README.md) + + +# Search API Integration + +For more information on the underlying API call and the associated parameters, please refer to the related: +[Bloomreach Search and Merchandising APIs page](https://documentation.bloomreach.com/discovery/reference/bloomreach-search-and-merchandising-apis). + +### Initializing the Product API object: + +[Kotlin] +``` +val coreApi = BrCoreApi.Builder() + .accountId("") + .uuid("") + .visitorType(VisitorType.NEW_USER) + .domainKey("") + .authKey("AUTH_KEY") + .userId("USER_ID") + .environment(Env.STAGE) + .build() +``` + +[Java] +``` +BrCoreApi coreApi = new BrCoreApi.Builder() + .accountId("") + .uuid("") + .visitorType(VisitorType.NEW_USER) + .domainKey("") + .authKey("AUTH_KEY") + .userId("USER_ID") + .environment(Env.STAGE) + .build(); +``` + + + + +| Parameter | Description | +| ------------- |--------------------------------------------------------------------------------------------| +| accountId | Account ID provided by Bloomreach | +| uuid | A 13 digit random number | +| visitorType | ENUM type for New User or returning user. | +| domainKey | Your site domain's ID, which is provided by Bloomreach. | +| authKey | The Bloomreach-provided authentication key | +| userId | The Bloomreach-provided authentication key | +| environment | ENUM to specify APIs to be pointed to which environment. STAGE or PROD. Defaulted to STAGE | +| baseUrl | The base url of the API env if its different the default| +| connectionTimeOut | Connection timeout in milliseconds | +| maxTotalConnections | Max total connections | +| responseTimeout | Connection timeout for getting a response | + + +### Product Search + +Create the object of ProductSearchRequest for the request parameter to be passed to the Product search API with different types of fields supported. + +``` +[Kotlin] + +val productSearchRequest = ProductSearchRequest() + .fl(listOf("pid", "title", "brand", "price")) + .searchTerm("table") + .url("example.com") + +val response = coreApi?.productSearchApi(productSearchRequest) + if(response is CoreResponse) { + //gets required response in response object of type CoreResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } + +``` + +``` +[Java] + +ProductSearchRequest productSearchRequest = new ProductSearchRequest() + .fl(Arrays.asList("pid", "title", "brand", "price")) + .searchTerm("test") + .url("example.com"); + +//Call the productSearchApi method and pass the request object. + + Object response = coreApi.productSearchApi(productSearchRequest); + if(response instanceof CoreResponse) { + //gets required response in response object of type CoreResponse + } else if(response instanceof BrApiError) { + // if the API fails, handle error here. + } +``` +Supported parameters for creating ProductSearchRequest object + +| Parameter | Method calls | +|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| rows | .rows(10) | +| start | .start(0) | +| url | .url(“example.com”) | +| fl | Can be set using below ways

.fl(“pid,title”)
.fl(listOf(“pid”, “title”))
.fl(arrayOf(“pid”, “title”)) | +| searchTerm | .searchTerm(“test”) | +| fq | .fq("color:\"red\"")
.fq("color", listOf("blue", "red"))
.fq("color", "red") | +| stats.field | .statsField("sale_price,length,width")
.statsField(listOf("sale_price","length","width")) | +| efq | .efq(“attribute:(\“value\”)”)
efq(attribute= "Fabric", value= “Cotton”)
//efq with NOT
.efq(attribute= "Fabric", value= “Cotton”, isNot=true)
//single attribute multiple values
.efq(attribute= "Fabric", values = listOf("Cotton", "Linen"), operator= Operator.OR)
//multiple attributes with operator
.efq(values = mapOf("Fabric" to "Cotton", "Color" to "Red"), operator= Operator.AND) | +| facet.range | .facetRange(“price”)
.facetRange(listOf("price","rating")) | +| facet.prefix | Max total connections | +| sort | //using simple string
.sort("price+asc")

//using Sort Object
.sort(Sort("price", SortOrder.ASCENDING)) | +| user_id | .userId(“usr123”) | +| view_id | .viewId(“”) | +| widget_id | .widgetId(“widget123”) | +| ll | //[BOPIS]
.latLong(“38.880657,-77.396935”) | +| fl | //[BOPIS]
.fl(“store_lat_lon,pid,title”) | +| fq | //[BOPIS]
.fq("store_lat_lon:\"100\"") | + + +### Category Search + +Create the object of CategorySearchRequest for the request parameter to be passed to the Category search API with different types of fields supported. + +``` +val categorySearchRequest = CategorySearchRequest() + .fl( + listOf( + "pid", + "title", + "brand", + "price" + ) + ) + .searchTerm("test") +.url("example.com") + + +//Call the categorySearchApi method and pass the request object. + +val response = coreApi?.categorySearchApi(categorySearchRequest) + if(response is CoreResponse) { + //gets required response in response object of type CoreResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + + } +``` +Note: Refer other parameters same as Product Search Parameters + + +### Content Search + +Create the object of ContentSearchRequest for the request parameter to be passed to the Content search API with different types of fields supported. + +``` +val contentSearchRequest = ContentSearchRequest() + .fl( + listOf( + "pid", + "title", + "brand", + "price" + ) + ) + .catalogName("Chair") + .searchTerm("test") +.url("example.com") + + +Call the contentSearchApi method and pass the request object. + +val response = coreApi?.contentSearchApi(contentSearchRequest) + if(response is CoreResponse) { + //gets required response in response object of type CoreResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } +``` + +| Parameter | Method calls | +|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| catalog_name | .catalogName(“catName”) | +Note: Refer other parameters same as Product Search Parameters + +### BestSeller API +Create the object of BestSellerRequest for the request parameter to be passed to the BestSeller API with different types of fields supported. + +``` +val bestSellerRequest = BestSellerRequest() + .fl( + listOf( + "pid", + "title", + "brand", + "price" + ) + ) + .searchTerm("test") + .url("example.com") + +//Call the bestSellerApi method and pass the request object. + +val response = coreApi?.bestSellerApi(bestSellerRequest) + if(response is CoreResponse) { + //gets required response in response object of type CoreResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } + +``` + +| Parameter | Method calls | +|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| title | .title(“productName”)| +Note: Refer other parameters same as Product Search Parameters + +### Thematic API + +For more information on the format of the JSON String, please refer to the [Thematic Pages API](https://documentation.bloomreach.com/discovery/reference/thematic-pages-api) page. + +Create the object of ThematicRequest for the request parameter to be passed to the Thematic API with different types of fields supported. + +``` +val thematicRequest = ThematicRequest().fl("pid") +.searchTerm("jack chains") + .url("example.com") + +..Call the thematicApi method and pass the request object. + +val response = coreApi?.thematicApi(thematicRequest) + if(response is CoreResponse) { + //gets required response in response object of type CoreResponse + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } +``` diff --git a/SEO_WIDGET.md b/SEO_WIDGET.md new file mode 100644 index 0000000..63f655d --- /dev/null +++ b/SEO_WIDGET.md @@ -0,0 +1,77 @@ +[GO BACK](README.md) + + + +# SEO WIDGET API Integration + +For more information on the format of the JSON String, please refer to the [SEO WIDGET API](https://documentation.bloomreach.com/discovery/reference/widget-api) page. + +### Initializing the BrSeoWidgetApi object: +``` +val seoWidgetApi = BrSeoWidgetApi.Builder() +.accountId("") + .uuid("") + .visitorType(VisitorType.NEW_USER) + .domainKey("") + .authKey("AUTH_KEY") + .userId("USER_ID") + .environment(Env.STAGE) + .build() + + +``` + +| Parameter | Description | +| ------------- |--------------------------------------------------------------------------------------------| +| accountId | Account ID provided by Bloomreach | +| uuid | A 13 digit random number | +| visitorType | ENUM type for New User or returning user. | +| domainKey | Your site domain's ID, which is provided by Bloomreach. | +| authKey | The Bloomreach-provided authentication key | +| userId | The Bloomreach-provided authentication key | +| environment | ENUM to specify APIs to be pointed to which environment. STAGE or PROD. Defaulted to STAGE | +| baseUrl | The base url of the API env if its different the default| +| connectionTimeOut | Connection timeout in milliseconds | +| maxTotalConnections | Max total connections | +| responseTimeout | Connection timeout for getting a response | + + +### SEO WIDGET API + +Create the object of SeoWidgetRequest for the request parameter to be passed to the SEO WIDGET API with different types of fields supported. + +``` + +val seoWidgetRequest = SeoWidgetRequest().acctAuth("") + .pType("product") + .url("example.com") + .prodId("p123") + + +//If 2nd parameter is passed as ResponseType.HTML then response will return HTML string else will return WidgetResponse object + + val response = seoWidgetApi?.seoWidgetApi(request, ResponseType.HTML) + + if(response is WidgetResponse) { + //gets required response in response object of type WidgetResponse + } + else if(response is String){ + // response as HTML string + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } + + +``` +Supported parameters for creating SeoWidgetRequest object + +| Parameter | Method calls | +|---------------|----------------------------------------------------------------------------------------------------------| +| acct_auth | .acctAuth() | +| ptype | .pType()| +| prod_id | ..prodId() | +| prod_name | .prodName() | +| pstatus | .pStatus() | +| url | .url() | +| user_agent | .userAgent() | diff --git a/SUGGEST.md b/SUGGEST.md new file mode 100644 index 0000000..7fbe449 --- /dev/null +++ b/SUGGEST.md @@ -0,0 +1,66 @@ +[GO BACK](README.md) + +# Auto Suggest API Integration + +For more information on the format of the JSON String, please refer to the [Autosuggest API](https://documentation.bloomreach.com/discovery/reference/autosuggest-api) page. + +### Initializing the Autosuggest API object: +``` +val brSuggestApi = BrSuggestApi.Builder() +.accountId("") + .uuid("") + .visitorType(VisitorType.NEW_USER) + .domainKey("") + .authKey("AUTH_KEY") + .userId("USER_ID") + .environment(Env.STAGE) + .build() + +``` + +| Parameter | Description | +| ------------- |--------------------------------------------------------------------------------------------| +| accountId | Account ID provided by Bloomreach | +| uuid | A 13 digit random number | +| visitorType | ENUM type for New User or returning user. | +| domainKey | Your site domain's ID, which is provided by Bloomreach. | +| authKey | The Bloomreach-provided authentication key | +| userId | The Bloomreach-provided authentication key | +| environment | ENUM to specify APIs to be pointed to which environment. STAGE or PROD. Defaulted to STAGE | +| baseUrl | The base url of the API env if its different the default| +| connectionTimeOut | Connection timeout in milliseconds | +| maxTotalConnections | Max total connections | +| responseTimeout | Connection timeout for getting a response | + + +### Autosuggest API + +Create the object of AutosuggestRequest for the request parameter to be passed to the Autosuggest API with different types of fields supported. + +``` + +val autoSuggestRequest = AutosuggestRequest() + .catalogViews(mapOf("product" to "store", "p1" to "sq")) + .url("example.com") + + +//Call the autoSuggestApi method and pass the request object. + +val response = suggestApi?.autoSuggestApi(autoSuggestRequest) + if(response is AutosuggestRequest) { + //gets required response in response object of type AutosuggestRequest + } else { + val error = response as BrApiError + // if the API fails, handle error here. + } + +``` +Supported parameters for creating ProductSearchRequest object + +| Parameter | Method calls | +|---------------|----------------------------------------------------------------------------------------------------------------| +| catalog_views | .catalogViews(“product:store1`|`p1:s1”)

.catalogViews(mapOf("product" to "store", "p1" to "s1")) | +| q | .searchTerm(“test”) | +| user_agent | .userAgent(“”) | +| user_id | .userId(“”) | +| url | .url(“”) | diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..08d4513 --- /dev/null +++ b/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + DiscoveryClientTools + org.example + 1.0-SNAPSHOT + jar + + consoleApp + + + UTF-8 + official + 1.8 + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + 1.8.0 + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + + + + org.apache.httpcomponents.client5 + httpclient5 + 5.1.1 + + + org.jetbrains.kotlin + kotlin-test-junit5 + 1.8.0 + test + + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.8.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.14.0 + + + + + + + \ No newline at end of file diff --git a/src/main/kotlin/.gitignore b/src/main/kotlin/.gitignore new file mode 100644 index 0000000..4c330e9 --- /dev/null +++ b/src/main/kotlin/.gitignore @@ -0,0 +1,156 @@ +#ignored project files +src/main/kotlin/Main.kt +src/main/kotlin/TestJmeter.kt +src/main/kotlin/TestJava.java +src/main/resources/ +# Default ignored files +/shelf/ +/workspace.xml +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Kotlin template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + diff --git a/src/main/kotlin/ApiConstants.kt b/src/main/kotlin/ApiConstants.kt new file mode 100644 index 0000000..3667a99 --- /dev/null +++ b/src/main/kotlin/ApiConstants.kt @@ -0,0 +1,64 @@ +/** + * API module constants including request parameters + */ +internal object ApiConstants { + + const val REQUEST_TYPE = "request_type" + const val SEARCH_TYPE = "search_type" + const val CATEGORY_TYPE = "category_type" + + const val REQUEST_TYPE_SEARCH = "search" + const val REQUEST_TYPE_THEMATIC = "thematic" + const val REQUEST_TYPE_SUGGEST = "suggest" + + const val SEARCH_TYPE_KEYWORD = "keyword" + const val SEARCH_TYPE_CATEGORY = "category" + const val CATEGORY_TYPE_DYNAMIC = "dynamic" + const val SEARCH_TYPE_BESTSELLER = "bestseller" + + const val ROWS = "rows" + const val DEFAULT_ROWS = 10 + + const val START = "start" + const val DEFAULT_START = 0 + + const val SEARCH_TERM = "q" + const val FL = "fl" + const val FQ = "fq" + const val SORT = "sort" + const val STATS_FIELD = "stats.field" + const val EFQ = "efq" + const val LAT_LONG = "ll" + const val FACET_RANGE = "facet.range" + + const val USER_ID = "user_id" + const val USER_IP = "user_ip" + const val VIEW_ID = "view_id" + const val WIDGET_ID = "widget_id" + + const val ITEM_IDS = "item_ids" + const val CAT_ID = "cat_id" + const val QUERY = "query" + const val CATALOG_NAME = "catalog_name" + const val TITLE = "title" + const val URL = "url" + const val CATALOG_VIEWS = "catalog_views" + const val USER_AGENT = "user_agent" + const val CONTEXT_ID = "context_id" + const val FIELDS = "fields" + const val FILTER_FACET = "filter_facet" + const val FACET = "facet" + const val FILTER = "filter" + + const val DEFAULT_FACET_FLAG = false + + const val ACCT_AUTH = "acct_auth" + const val P_TYPE = "ptype" + const val PROD_ID = "prod_id" + const val PROD_NAME = "prod_name" + const val P_STATUS = "pstatus" + const val REF_URL = "ref_url" + + const val IMAGE_ID = "image_id" + +} \ No newline at end of file diff --git a/src/main/kotlin/BrApiClient.kt b/src/main/kotlin/BrApiClient.kt new file mode 100644 index 0000000..3f8c1f9 --- /dev/null +++ b/src/main/kotlin/BrApiClient.kt @@ -0,0 +1,69 @@ +import enums.Env +import enums.VisitorType +import network.BrHttpClient +import org.apache.hc.client5.http.config.RequestConfig +import org.apache.hc.client5.http.impl.classic.HttpClients +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager +import org.apache.hc.core5.util.Timeout +import java.util.concurrent.TimeUnit + +/** + * Abstract Class for building different type of API objects with some common parameters. + */ +abstract class BrApiClient(private val brApiRequest: BrApiRequest) { + + internal var brHttpClient: BrHttpClient + + init { + //Common Pooling manager for API calls + val poolingConnManager = PoolingHttpClientConnectionManager() + poolingConnManager.maxTotal = brApiRequest.maxTotalConnections + + // maxTotal – Set the maximum number of total open connections + poolingConnManager.maxTotal = brApiRequest.maxTotalConnections + +// Timeout + val timeout = Timeout.of(brApiRequest.connectionTimeOut, TimeUnit.MILLISECONDS) + + val config: RequestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setConnectionRequestTimeout(timeout) + .setResponseTimeout(brApiRequest.responseTimeout, TimeUnit.MILLISECONDS) + .build() + +// set pooling and config paramters to client + val client = HttpClients.custom() + .setConnectionManager(poolingConnManager) + .setDefaultRequestConfig(config) + .build() + + brHttpClient = BrHttpClient(client) + } + + abstract class Builder( + var accountId: String? = null, + var uuid: String? = null, + var visitorType: VisitorType? = null, + var domainKey: String? = null, + var authKey: String? = null, + var userId: String? = null, + var environment: Env = Env.STAGE, + var baseUrl: String? = null, + var connectionTimeOut: Long = 5000, + var maxTotalConnections: Int = 10, + var responseTimeout: Long = 5000 + ) { + fun accountId(accountId: String) = apply { this.accountId = accountId } + fun uuid(uuid: String) = apply { this.uuid = uuid } + fun visitorType(visitorType: VisitorType) = apply { this.visitorType = visitorType } + fun domainKey(domainKey: String) = apply { this.domainKey = domainKey } + fun authKey(authKey: String) = apply { this.authKey = authKey } + fun userId(userId: String) = apply { this.userId = userId } + fun environment(environment: Env) = apply { this.environment = environment } + fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl } + fun connectionTimeOut(connectionTimeOut: Long) = apply { this.connectionTimeOut = connectionTimeOut } + fun maxTotalConnections(maxTotalConnections: Int) = apply { this.maxTotalConnections = maxTotalConnections } + fun responseTimeout(responseTimeout: Long) = apply { this.responseTimeout = responseTimeout } + abstract fun build(): T? + } +} \ No newline at end of file diff --git a/src/main/kotlin/BrApiRequest.kt b/src/main/kotlin/BrApiRequest.kt new file mode 100644 index 0000000..866f47b --- /dev/null +++ b/src/main/kotlin/BrApiRequest.kt @@ -0,0 +1,31 @@ +import enums.Env +import enums.VisitorType + +/** + * Class containing initialising parameters for the API SDK. + * + * @property accountId Account Id provided by Bloomreach + * @property uuid Android Advertising ID + * @property visitorType ENUM type for New User or returning user + * @property domainKey The Bloomreach-provided ID of the domain receiving the request. + * @property authKey This parameter is only required if you track users via a universal customer ID. + * @property userId This parameter is only required if you track users via a universal customer ID. + * @property environment ENUM for api to be pointed to which version., STAGE or PROD + * @property baseUrl String for base URL + * @property connectionTimeOut Connection timeout in millis + * @property maxTotalConnections Max total connections + * @property responseTimeout Connection timeout for getting response + */ +data class BrApiRequest( + val accountId: String?, + val uuid: String?, + val visitorType: VisitorType?, + val domainKey: String?, + var authKey: String? = null, + var userId: String? = null, + var environment: Env = Env.STAGE, + var baseUrl: String? = null, + var connectionTimeOut: Long = 5000, + var maxTotalConnections: Int = 10, + var responseTimeout: Long = 5000 +) \ No newline at end of file diff --git a/src/main/kotlin/BrCoreApi.kt b/src/main/kotlin/BrCoreApi.kt new file mode 100644 index 0000000..37f912b --- /dev/null +++ b/src/main/kotlin/BrCoreApi.kt @@ -0,0 +1,87 @@ +import enums.ResponseType +import network.ApiProcessor +import request.* + +/** + * BrCoreApi class holds method to initiate BrApiClient object and API calls methods + */ +class BrCoreApi private constructor( + brApiRequest: BrApiRequest) : BrApiClient(brApiRequest) { + + // create object for ApiProcessor + private val apiProcessor = ApiProcessor(brApiRequest, brHttpClient) + + class Builder : BrApiClient.Builder() { + /** + * Method to create object for BrCoreApi + * + * @return BrCoreApi instance + */ + override fun build(): BrCoreApi { + return BrCoreApi(BrApiRequest(accountId, uuid, visitorType, domainKey, authKey, userId, environment, baseUrl)) + } + } + + /** + * Method for calling Product Search API request + * @param productSearchRequest Request Object required for Product Search API + * + * @return Any response object, CoreResponse if API call success else return BrApiError object + */ + public fun productSearchApi(productSearchRequest: ProductSearchRequest): Any? { + return apiProcessor.processCoreApi(productSearchRequest.getMap()) + } + + /** + * Method for calling Category Search API request + * @param categorySearchRequest Request Object required for Category Search API + * + * @return Any response object, CoreResponse if API call success else return BrApiError object + */ + public fun categorySearchApi(categorySearchRequest: CategorySearchRequest): Any? { + return apiProcessor.processCoreApi(categorySearchRequest.getMap()) + } + + /** + * Method for calling Category Search API request + * @param categorySearchRequest Request Object required for Category Search API + * + * @return Any response object, CoreResponse if API call success else return BrApiError object + */ + public fun dynamicCategorySearchApi(categorySearchRequest: CategorySearchRequest): Any? { + categorySearchRequest.setDynamicCategory() + return apiProcessor.processCoreApi(categorySearchRequest.getMap()) + } + + /** + * Method for calling Content API request + * @param contentSearchRequest Request Object required for Content Search API + * + * @return Any response object, CoreResponse if API call success else return BrApiError object + */ + public fun contentSearchApi(contentSearchRequest: ContentSearchRequest): Any? { + return apiProcessor.processCoreApi(contentSearchRequest.getMap()) + } + + /** + * Method for calling BestSeller API request + * @param bestSellerRequest Request Object required for Content Search API + * + * @return Any response object, CoreResponse if API call success else return BrApiError object + */ + public fun bestSellerApi(bestSellerRequest: BestSellerRequest):Any? { + return apiProcessor.processCoreApi(bestSellerRequest.getMap()) + } + + /** + * Method for calling Thematic API request + * @param thematicRequest Request Object required for Thematic API + * @param responseType Specify Response needed in JSON or HTML format, defaulted to JSON + * + * @return Any response object, CoreResponse if API call success else return BrApiError object + */ + public fun thematicApi(thematicRequest: ThematicRequest, responseType: ResponseType = ResponseType.JSON): Any? { + return apiProcessor.processThematicApi(thematicRequest.getMap(), responseType) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/BrRecsAndPathwaysApi.kt b/src/main/kotlin/BrRecsAndPathwaysApi.kt new file mode 100644 index 0000000..8c42243 --- /dev/null +++ b/src/main/kotlin/BrRecsAndPathwaysApi.kt @@ -0,0 +1,156 @@ +import enums.WidgetApiType +import network.ApiProcessor +import request.WidgetRequest +import java.io.InputStream + +/** + * BrRecsAndPathwaysApi class holds method to initiate BrApiClient object and API calls methods + */ +class BrRecsAndPathwaysApi private constructor( + brApiRequest: BrApiRequest) : BrApiClient(brApiRequest) { + + private val apiProcessor = ApiProcessor(brApiRequest, brHttpClient) + + class Builder : BrApiClient.Builder() { + /** + * @return BrRecsAndPathwaysApi instance + */ + override fun build(): BrRecsAndPathwaysApi { + return BrRecsAndPathwaysApi(BrApiRequest(accountId, uuid, visitorType, domainKey, authKey, userId, environment, baseUrl)) + } + } + + /** + * Method for calling Recommendation Widget API where apiType can be specified + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param apiType the type of Recommendation Widget API. This is the widgetType path parameter + * @param widgetRequest request Object required for Global Recommendation Widget API + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + public fun recAndPathwaysWidgetApi(widgetId: String, apiType: WidgetApiType, widgetRequest: WidgetRequest): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + + return recAndPathwaysWidgetApi(widgetId, apiType.value, widgetRequest) + } + + /** + * Method for calling Recommendation Widget API where apiType can be specified + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param apiType the type of Recommendation Widget API. This is the widgetType path parameter + * @param widgetRequest request Object required for Global Recommendation Widget API + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + private fun recAndPathwaysWidgetApi(widgetId: String, + apiType:String, widgetRequest: WidgetRequest): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + return apiProcessor.processRecsAndPathwaysApi(widgetId, apiType, widgetRequest.getMap()) + } + + /** + * Method for calling Item-based Recommendation Widget API + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param widgetRequest request Object required for Item-based Recommendation Widget API + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + public fun itemBasedRecommendationWidgetApi(widgetId: String, widgetRequest: WidgetRequest): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + return recAndPathwaysWidgetApi(widgetId, WidgetApiType.ITEM.value, widgetRequest) + } + + /** + * Method for calling Category-based Recommendation Widget API + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param widgetRequest request Object required for Category-based Recommendation Widget API + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + public fun categoryBasedWidgetApi(widgetId: String, widgetRequest: WidgetRequest): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + return recAndPathwaysWidgetApi(widgetId, WidgetApiType.CATEGORY.value, widgetRequest) + } + + /** + * Method for calling Keyword-based Widget API + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param widgetRequest request Object required for Keyword-based Recommendation Widget API + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + + public fun keywordBasedWidgetApi(widgetId: String, widgetRequest: WidgetRequest): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + return recAndPathwaysWidgetApi(widgetId, WidgetApiType.KEYWORD.value, widgetRequest) + } + + + /** + * Method for calling Personalization-based Widget API + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param widgetRequest request Object required for Personalization-based Recommendation Widget API + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + + public fun personalizationBasedWidgetApi(widgetId: String, widgetRequest: WidgetRequest): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + return recAndPathwaysWidgetApi(widgetId, WidgetApiType.PERSONALIZED.value, widgetRequest) + } + + /** + * Method for calling Global Recommendation Widget API + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param widgetRequest request Object required for Global Recommendation Widget API + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + public fun globalRecommendationWidgetApi(widgetId: String, widgetRequest: WidgetRequest): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + return recAndPathwaysWidgetApi(widgetId, WidgetApiType.GLOBAL.value, widgetRequest) + } + + /** + * Method to call Image Upload API for visual search and invoke the callback with appropriate result + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param inputStream inputStream of the image + * @param fileName file name of the image + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + fun uploadImageForVisualSearch(widgetId: String, inputStream: InputStream, fileName:String): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + return apiProcessor.processVisualSearchUploadApi(widgetId, inputStream, fileName) + } + + /** + * Method for calling Visual Search Widget API + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param widgetRequest request Object required for Global Recommendation Widget API + * + * @return Any response object, RecsAndPathwaysResponse if API call success else return BrApiError object + */ + fun visualSearchWidgetApi(widgetId: String, widgetRequest: WidgetRequest): Any? { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + return recAndPathwaysWidgetApi(widgetId, WidgetApiType.VISUAL_SEARCH.value, widgetRequest) + } +} \ No newline at end of file diff --git a/src/main/kotlin/BrSeoWidgetApi.kt b/src/main/kotlin/BrSeoWidgetApi.kt new file mode 100644 index 0000000..2b8b03c --- /dev/null +++ b/src/main/kotlin/BrSeoWidgetApi.kt @@ -0,0 +1,48 @@ +import enums.ResponseType +import network.ApiProcessor +import request.SeoWidgetRequest + +/** + * BrSeoWidgetApi class holds method to initiate BrApiClient object and API calls methods + */ +class BrSeoWidgetApi private constructor( + brApiRequest: BrApiRequest +) : BrApiClient(brApiRequest) { + + private val apiProcessor = ApiProcessor(brApiRequest, brHttpClient) + + class Builder : BrApiClient.Builder() { + /** + * @return BrSeoWidgetApi instance + */ + override fun build(): BrSeoWidgetApi? { + return BrSeoWidgetApi( + BrApiRequest( + accountId, + uuid, + visitorType, + domainKey, + authKey, + userId, + environment, + baseUrl + ) + ) + } + } + + /** + * Method for calling SEO Widget API request + * @param endpoint Base Url for the API call + * @param seoWidgetRequest Base Url for the API call + * @param responseType Specify Response needed in JSON or HTML format, defaulted to JSON + * + * @return Any response object, WidgetResponse or HTML if API call success else return BrApiError object. + */ + public fun seoWidgetApi( + seoWidgetRequest: SeoWidgetRequest, + responseType: ResponseType = ResponseType.JSON + ): Any? { + return apiProcessor.processWidgetApi(seoWidgetRequest.getMap(), responseType) + } +} \ No newline at end of file diff --git a/src/main/kotlin/BrSuggestApi.kt b/src/main/kotlin/BrSuggestApi.kt new file mode 100644 index 0000000..c77c453 --- /dev/null +++ b/src/main/kotlin/BrSuggestApi.kt @@ -0,0 +1,30 @@ +import network.ApiProcessor +import request.AutosuggestRequest + +/** + * BrSuggestApi class holds method to initiate BrApiClient object and API calls methods + */ +class BrSuggestApi private constructor( + brApiRequest: BrApiRequest) : BrApiClient(brApiRequest) { + + private val apiProcessor = ApiProcessor(brApiRequest, brHttpClient) + + class Builder : BrApiClient.Builder() { + /** + * @return BrSuggestApi instance + */ + override fun build(): BrSuggestApi { + return BrSuggestApi(BrApiRequest(accountId, uuid, visitorType, domainKey, authKey, userId, environment, baseUrl)) + } + } + + /** + * Method for calling Suggest API request + * @param autoSuggestRequest Request Object required for AutosuggestRequest API + * + * @return Any response object, SuggestResponse if API call success else return BrApiError object. + */ + public fun autoSuggestApi(autoSuggestRequest: AutosuggestRequest): Any? { + return apiProcessor.processSuggestApi(autoSuggestRequest.getMap()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/enums/ApiType.kt b/src/main/kotlin/enums/ApiType.kt new file mode 100644 index 0000000..b024913 --- /dev/null +++ b/src/main/kotlin/enums/ApiType.kt @@ -0,0 +1,11 @@ +package enums + +/** + * ENUM to identify the type of API internally + */ +internal enum class ApiType { + CORE, + SUGGEST, + PATHWAYS, + WIDGET +} \ No newline at end of file diff --git a/src/main/kotlin/enums/Env.kt b/src/main/kotlin/enums/Env.kt new file mode 100644 index 0000000..e5cb025 --- /dev/null +++ b/src/main/kotlin/enums/Env.kt @@ -0,0 +1,9 @@ +package enums + +/** + * ENUM to specify APIs to be pointed to which environment + */ +enum class Env { + STAGE, + PROD +} \ No newline at end of file diff --git a/src/main/kotlin/enums/ResponseType.kt b/src/main/kotlin/enums/ResponseType.kt new file mode 100644 index 0000000..c1d14b3 --- /dev/null +++ b/src/main/kotlin/enums/ResponseType.kt @@ -0,0 +1,9 @@ +package enums + +/** + * ENUM to specify the type of Response Needed + */ +enum class ResponseType { + JSON, + HTML +} \ No newline at end of file diff --git a/src/main/kotlin/enums/VisitorType.kt b/src/main/kotlin/enums/VisitorType.kt new file mode 100644 index 0000000..c2531eb --- /dev/null +++ b/src/main/kotlin/enums/VisitorType.kt @@ -0,0 +1,9 @@ +package enums + +/** + * VisitorType ENUM to provide hitcount based on new or returning user + */ +enum class VisitorType(val hitCount: Int) { + NEW_USER(1), + RETURNING_USER(2) +} \ No newline at end of file diff --git a/src/main/kotlin/enums/WidgetApiType.kt b/src/main/kotlin/enums/WidgetApiType.kt new file mode 100644 index 0000000..6f15e3a --- /dev/null +++ b/src/main/kotlin/enums/WidgetApiType.kt @@ -0,0 +1,14 @@ +package enums + +/** + * Widget TYPE ENUM to specify which type on widget API needs to be called. + * This gets added as Path parameter to the request + */ +enum class WidgetApiType(val value: String) { + ITEM("item"), + CATEGORY("category"), + KEYWORD("keyword"), + PERSONALIZED("personalized"), + GLOBAL("global"), + VISUAL_SEARCH("visual/search") +} \ No newline at end of file diff --git a/src/main/kotlin/model/BaseResponse.kt b/src/main/kotlin/model/BaseResponse.kt new file mode 100644 index 0000000..455826d --- /dev/null +++ b/src/main/kotlin/model/BaseResponse.kt @@ -0,0 +1,18 @@ +package model + +import com.fasterxml.jackson.annotation.JsonAnyGetter +import com.fasterxml.jackson.annotation.JsonAnySetter + +open class BaseResponse { + + @JsonAnySetter + @get:JsonAnyGetter + val otherFields: Map = hashMapOf() + + fun getOtherField(key: String): Any? { + return if (otherFields.contains(key)) { + otherFields[key] + } else + null + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/BrApiError.kt b/src/main/kotlin/model/BrApiError.kt new file mode 100644 index 0000000..a182202 --- /dev/null +++ b/src/main/kotlin/model/BrApiError.kt @@ -0,0 +1,17 @@ +package model + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Generic Error class for handling errors from API calls + * + * @property errorMessage Formatted error message + * @property errorCode Error code for additional handling + */ +data class BrApiError( + @JsonProperty("message") + val errorMessage: String, + + @JsonProperty("status_code") + val errorCode: Int +) diff --git a/src/main/kotlin/model/core/Campaign.kt b/src/main/kotlin/model/core/Campaign.kt new file mode 100644 index 0000000..734d32d --- /dev/null +++ b/src/main/kotlin/model/core/Campaign.kt @@ -0,0 +1,13 @@ +package model.core + +import model.BaseResponse + +data class Campaign( + val id: String? = null, + val htmlText: String? = null, + val bannerType: String? = null, + val keyword: String? = null, + val name: String? = null, + val dateEnd: String? = null, + val dateStart: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/core/CoreResponse.kt b/src/main/kotlin/model/core/CoreResponse.kt new file mode 100644 index 0000000..6b57202 --- /dev/null +++ b/src/main/kotlin/model/core/CoreResponse.kt @@ -0,0 +1,40 @@ +package model.core + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class CoreResponse( + @JsonProperty("category_map") + val categoryMap: LinkedHashMap? = null, + + @JsonProperty("did_you_mean") + val didYouMean: List? = null, + + @JsonProperty("facet_counts") + val facetCounts: FacetCounts? = null, + + @JsonProperty("response") + val response: Response? = null, + + @JsonProperty("campaign") + val campaign: Campaign? = null, + + @JsonProperty("stats") + val stats: Stats? = null, + + @JsonProperty("keywordRedirect") + val keywordRedirect: KeywordRedirect? = null, + + @JsonProperty("Metadata") + val metadata: Metadata? = null, + + @JsonProperty("autoCorrectQuery") + val autoCorrectQuery: String? = null +) : BaseResponse() { + fun getCategory(key: String): String? { + return if (categoryMap?.contains(key) == true) { + categoryMap[key] + } else + null + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/core/Doc.kt b/src/main/kotlin/model/core/Doc.kt new file mode 100644 index 0000000..90f0617 --- /dev/null +++ b/src/main/kotlin/model/core/Doc.kt @@ -0,0 +1,33 @@ +package model.core + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Doc( + @JsonProperty("brand") + val brand: String? = null, + + @JsonProperty("description") + val description: String? = null, + + @JsonProperty("pid") + val pid: String? = null, + + @JsonProperty("price") + val price: Double? = null, + + @JsonProperty("sale_price") + val salePrice: Double? = null, + + @JsonProperty("thumb_image") + val thumbImage: String? = null, + + @JsonProperty("title") + val title: String? = null, + + @JsonProperty("url") + val url: String? = null, + + @JsonProperty("variants") + val variants: List? = null, +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/core/Facet.kt b/src/main/kotlin/model/core/Facet.kt new file mode 100644 index 0000000..7979f57 --- /dev/null +++ b/src/main/kotlin/model/core/Facet.kt @@ -0,0 +1,14 @@ +package model.core + +import com.fasterxml.jackson.annotation.JsonFormat +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty +data class Facet( + @JsonProperty("name") + var name: String? = null, + @JsonProperty("type") + var type: String? = null, + @JsonFormat(with = arrayOf(JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)) + @JsonProperty("value") + var value: ArrayList? = null +): BaseResponse() diff --git a/src/main/kotlin/model/core/FacetCounts.kt b/src/main/kotlin/model/core/FacetCounts.kt new file mode 100644 index 0000000..78a5e58 --- /dev/null +++ b/src/main/kotlin/model/core/FacetCounts.kt @@ -0,0 +1,26 @@ +package model.core + +import com.fasterxml.jackson.annotation.JsonFormat +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class FacetCounts( + + @Deprecated("Customers whose go-live date is after September 7, 2023 will be on V3 Facet response format by default. " + + "If you’re on the legacy format and would like to implement the new Facet response format, kindly contact your Bloomreach Services representative.") + @JsonProperty("facet_fields") + val facetFields: LinkedHashMap>? = null, + + @Deprecated("Customers whose go-live date is after September 7, 2023 will be on V3 Facet response format by default. " + + "If you’re on the legacy format and would like to implement the new Facet response format, kindly contact your Bloomreach Services representative.") + @JsonProperty("facet_queries") + val facetQueries: LinkedHashMap>? = null, + + @Deprecated("Customers whose go-live date is after September 7, 2023 will be on V3 Facet response format by default. " + + "If you’re on the legacy format and would like to implement the new Facet response format, kindly contact your Bloomreach Services representative.") + @JsonProperty("facet_ranges") + val facetRanges: LinkedHashMap>? = null, + + @JsonProperty("facets") + val facets: List? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/core/FacetFields.kt b/src/main/kotlin/model/core/FacetFields.kt new file mode 100644 index 0000000..8c2f21c --- /dev/null +++ b/src/main/kotlin/model/core/FacetFields.kt @@ -0,0 +1,11 @@ +package model.core + +import com.fasterxml.jackson.databind.JsonDeserializer +import model.BaseResponse +import com.fasterxml.jackson.databind.annotation.JsonDeserialize + +@JsonDeserialize(using = JsonDeserializer.None::class) +class FacetFields : FacetValue() { + val name: String? = null +} + diff --git a/src/main/kotlin/model/core/FacetFieldsCategory.kt b/src/main/kotlin/model/core/FacetFieldsCategory.kt new file mode 100644 index 0000000..2dc3752 --- /dev/null +++ b/src/main/kotlin/model/core/FacetFieldsCategory.kt @@ -0,0 +1,23 @@ +package model.core + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.annotation.JsonDeserialize + +@JsonDeserialize(using = JsonDeserializer.None::class) +data class FacetFieldsCategory( + @JsonProperty("cat_id") + val catId: String? = null, + + @JsonProperty("cat_name") + val catName: String? = null, + + @JsonProperty("crumb") + val crumb: String? = null, + + @JsonProperty("tree_path") + val treePath: String? = null, + + @JsonProperty("parent") + val parent: String? = null, +) : FacetValue() diff --git a/src/main/kotlin/model/core/FacetRange.kt b/src/main/kotlin/model/core/FacetRange.kt new file mode 100644 index 0000000..3c54f1e --- /dev/null +++ b/src/main/kotlin/model/core/FacetRange.kt @@ -0,0 +1,11 @@ +package model.core + +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import model.BaseResponse + +@JsonDeserialize(using = JsonDeserializer.None::class) +data class FacetRange( + val start: Any? = null, + val end: Any? = null, +) : FacetValue() diff --git a/src/main/kotlin/model/core/FacetValue.kt b/src/main/kotlin/model/core/FacetValue.kt new file mode 100644 index 0000000..eca6125 --- /dev/null +++ b/src/main/kotlin/model/core/FacetValue.kt @@ -0,0 +1,31 @@ +package model.core + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize + +@JsonDeserialize(using = FacetValueDeserializer::class) +open class FacetValue : BaseResponse() { + +// @JsonProperty("cat_id") +// val catId: String? = null +// +// @JsonProperty("cat_name") +// val catName: String? = null +// +// @JsonProperty("crumb") +// val crumb: String? = null +// +// @JsonProperty("tree_path") +// val treePath: String? = null +// +// @JsonProperty("parent") +// val parent: String? = null +// +// val start: Any? = null +// val end: Any? = null +// +// val name: String? = null + + val count: Int? = null +} \ No newline at end of file diff --git a/src/main/kotlin/model/core/FacetValueDeserializer.kt b/src/main/kotlin/model/core/FacetValueDeserializer.kt new file mode 100644 index 0000000..c46d575 --- /dev/null +++ b/src/main/kotlin/model/core/FacetValueDeserializer.kt @@ -0,0 +1,28 @@ +package model.core + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.node.ObjectNode +import java.io.IOException + + +class FacetValueDeserializer : StdDeserializer(FacetValue::class.java) { + @Throws(IOException::class, JsonProcessingException::class) + override fun deserialize(jsonParser: JsonParser, deserializationContext: DeserializationContext): FacetValue { + val mapper = jsonParser.codec as ObjectMapper + val root = mapper.readTree(jsonParser) + // Determine the type based on existence of certain fields + val type: Class<*> + type = if (root.has("start")) { + FacetRange::class.java + } else if (root.has("cat_id")) { + FacetFieldsCategory::class.java + } else { + FacetFields::class.java + } + return mapper.treeToValue(root, type) as FacetValue + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/core/KeywordRedirect.kt b/src/main/kotlin/model/core/KeywordRedirect.kt new file mode 100644 index 0000000..97f1b25 --- /dev/null +++ b/src/main/kotlin/model/core/KeywordRedirect.kt @@ -0,0 +1,9 @@ +package model.core + +import model.BaseResponse + +data class KeywordRedirect( + val originalQuery: String? = null, + val redirectedQuery: String? = null, + val redirectedUrl: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/core/Metadata.kt b/src/main/kotlin/model/core/Metadata.kt new file mode 100644 index 0000000..0dc2d8e --- /dev/null +++ b/src/main/kotlin/model/core/Metadata.kt @@ -0,0 +1,10 @@ +package model.core + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Metadata( + @JsonProperty("query") + val query: Query? = null +) : BaseResponse() + diff --git a/src/main/kotlin/model/core/Modification.kt b/src/main/kotlin/model/core/Modification.kt new file mode 100644 index 0000000..a020922 --- /dev/null +++ b/src/main/kotlin/model/core/Modification.kt @@ -0,0 +1,12 @@ +package model.core + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Modification( + @JsonProperty("mode") + val mode: String? = null, + + @JsonProperty("value") + val value: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/core/Query.kt b/src/main/kotlin/model/core/Query.kt new file mode 100644 index 0000000..44e99cc --- /dev/null +++ b/src/main/kotlin/model/core/Query.kt @@ -0,0 +1,12 @@ +package model.core + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Query( + @JsonProperty("modification") + val modification: Modification? = null, + + @JsonProperty("didYouMean") + val didYouMean: List? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/core/Response.kt b/src/main/kotlin/model/core/Response.kt new file mode 100644 index 0000000..c7a8fe3 --- /dev/null +++ b/src/main/kotlin/model/core/Response.kt @@ -0,0 +1,15 @@ +package model.core + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Response( + @JsonProperty("docs") + val docs: List? = null, + + @JsonProperty("numFound") + val numFound: Int? = null, + + @JsonProperty("start") + val start: Int? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/core/Stats.kt b/src/main/kotlin/model/core/Stats.kt new file mode 100644 index 0000000..d8dd67f --- /dev/null +++ b/src/main/kotlin/model/core/Stats.kt @@ -0,0 +1,13 @@ +package model.core + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Stats( + @JsonProperty("stats_fields") + val statsFields: LinkedHashMap? = null +) : BaseResponse() { + fun getStatsField(key: String): StatsField { + return statsFields?.get(key)!! + } +} \ No newline at end of file diff --git a/src/main/kotlin/model/core/StatsField.kt b/src/main/kotlin/model/core/StatsField.kt new file mode 100644 index 0000000..a1846c6 --- /dev/null +++ b/src/main/kotlin/model/core/StatsField.kt @@ -0,0 +1,8 @@ +package model.core + +import model.BaseResponse + +data class StatsField( + val min: Double = 0.0, + val max: Double = 0.0 +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/core/Variant.kt b/src/main/kotlin/model/core/Variant.kt new file mode 100644 index 0000000..dcea74a --- /dev/null +++ b/src/main/kotlin/model/core/Variant.kt @@ -0,0 +1,8 @@ +package model.core +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Variant( + @JsonProperty("skuid") + val skuId: List? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/rp/Category.kt b/src/main/kotlin/model/rp/Category.kt new file mode 100644 index 0000000..cfb7823 --- /dev/null +++ b/src/main/kotlin/model/rp/Category.kt @@ -0,0 +1,18 @@ +package model.rp + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class Category( + @JsonProperty("cat_id") + val catId: String? = null, + + @JsonProperty("cat_name") + val catName: String? = null, + + @JsonProperty("count") + val count: Int = 0, + + @JsonProperty("children") + val children: List? = null +) : BaseResponse() diff --git a/src/main/kotlin/model/rp/Doc.kt b/src/main/kotlin/model/rp/Doc.kt new file mode 100644 index 0000000..84352c1 --- /dev/null +++ b/src/main/kotlin/model/rp/Doc.kt @@ -0,0 +1,34 @@ +package model.rp + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse +import model.core.Variant + +data class Doc( + @JsonProperty("brand") + val brand: String? = null, + + @JsonProperty("description") + val description: String? = null, + + @JsonProperty("pid") + val pid: String? = null, + + @JsonProperty("price") + val price: Double? = null, + + @JsonProperty("sale_price") + val salePrice: Double? = null, + + @JsonProperty("thumb_image") + val thumbImage: String? = null, + + @JsonProperty("title") + val title: String? = null, + + @JsonProperty("url") + val url: String? = null, + + @JsonProperty("variants") + val variants: List? = null, +) : BaseResponse() diff --git a/src/main/kotlin/model/rp/Facet.kt b/src/main/kotlin/model/rp/Facet.kt new file mode 100644 index 0000000..2f54e21 --- /dev/null +++ b/src/main/kotlin/model/rp/Facet.kt @@ -0,0 +1,15 @@ +package model.rp + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Facet( + @JsonProperty("category") + val category: List? = null, + + @JsonProperty("fields") + val fields: List? = null, + + @JsonProperty("ranges") + val ranges: List? = null, +) : BaseResponse() diff --git a/src/main/kotlin/model/rp/Field.kt b/src/main/kotlin/model/rp/Field.kt new file mode 100644 index 0000000..f10cc54 --- /dev/null +++ b/src/main/kotlin/model/rp/Field.kt @@ -0,0 +1,12 @@ +package model.rp + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Field( + @JsonProperty("key") + val key: String? = null, + + @JsonProperty("value") + val value: List? = null, +) : BaseResponse() diff --git a/src/main/kotlin/model/rp/Metadata.kt b/src/main/kotlin/model/rp/Metadata.kt new file mode 100644 index 0000000..d4a4f85 --- /dev/null +++ b/src/main/kotlin/model/rp/Metadata.kt @@ -0,0 +1,12 @@ +package model.rp + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Metadata( + @JsonProperty("response") + val response: MetadataResponse? = null, + + @JsonProperty("widget") + val widget: Widget? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/rp/MetadataResponse.kt b/src/main/kotlin/model/rp/MetadataResponse.kt new file mode 100644 index 0000000..a9aa43d --- /dev/null +++ b/src/main/kotlin/model/rp/MetadataResponse.kt @@ -0,0 +1,15 @@ +package model.rp + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class MetadataResponse( + @JsonProperty("fallback") + val fallback: String? = null, + + @JsonProperty("personalized_results") + val personalizedResults: Boolean? = null, + + @JsonProperty("recall") + val recall: String? = null +) : BaseResponse() diff --git a/src/main/kotlin/model/rp/Range.kt b/src/main/kotlin/model/rp/Range.kt new file mode 100644 index 0000000..053cfbd --- /dev/null +++ b/src/main/kotlin/model/rp/Range.kt @@ -0,0 +1,12 @@ +package model.rp + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Range( + @JsonProperty("key") + val key: String? = null, + + @JsonProperty("value") + val value: List? = null, +) : BaseResponse() diff --git a/src/main/kotlin/model/rp/RecsAndPathwaysResponse.kt b/src/main/kotlin/model/rp/RecsAndPathwaysResponse.kt new file mode 100644 index 0000000..f153a5d --- /dev/null +++ b/src/main/kotlin/model/rp/RecsAndPathwaysResponse.kt @@ -0,0 +1,12 @@ +package model.rp + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class RecsAndPathwaysResponse( + @JsonProperty("metadata") + val metadata: Metadata? = null, + + @JsonProperty("response") + val response: Response? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/rp/Response.kt b/src/main/kotlin/model/rp/Response.kt new file mode 100644 index 0000000..28d2f9b --- /dev/null +++ b/src/main/kotlin/model/rp/Response.kt @@ -0,0 +1,15 @@ +package model.rp + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Response( + @JsonProperty("docs") + val docs: List? = null, + + @JsonProperty("numFound") + val numFound: Int? = null, + + @JsonProperty("start") + val start: Int? = null +) : BaseResponse() diff --git a/src/main/kotlin/model/rp/RpError.kt b/src/main/kotlin/model/rp/RpError.kt new file mode 100644 index 0000000..2fda26a --- /dev/null +++ b/src/main/kotlin/model/rp/RpError.kt @@ -0,0 +1,8 @@ +package model.rp + +import com.fasterxml.jackson.annotation.JsonProperty + +data class RpError( + @JsonProperty("detail") + val detail: String? = null, +) diff --git a/src/main/kotlin/model/rp/Value.kt b/src/main/kotlin/model/rp/Value.kt new file mode 100644 index 0000000..f65eb06 --- /dev/null +++ b/src/main/kotlin/model/rp/Value.kt @@ -0,0 +1,19 @@ +package model.rp + +import model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Value( + @JsonProperty("name") + val name: String? = null, + + @JsonProperty("count") + val count: Int = 0, + + @JsonProperty("start") + val start: Any? = null, + + @JsonProperty("end") + val end: Any? = null, + +) : BaseResponse() diff --git a/src/main/kotlin/model/rp/Widget.kt b/src/main/kotlin/model/rp/Widget.kt new file mode 100644 index 0000000..7304bda --- /dev/null +++ b/src/main/kotlin/model/rp/Widget.kt @@ -0,0 +1,21 @@ +package model.rp + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class Widget( + @JsonProperty("description") + val description: String? = null, + + @JsonProperty("id") + val id: String? = null, + + @JsonProperty("name") + val name: String? = null, + + @JsonProperty("rid") + val rid: String? = null, + + @JsonProperty("type") + val type: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/suggest/AttributeSuggestion.kt b/src/main/kotlin/model/suggest/AttributeSuggestion.kt new file mode 100644 index 0000000..3bb1157 --- /dev/null +++ b/src/main/kotlin/model/suggest/AttributeSuggestion.kt @@ -0,0 +1,15 @@ +package model.suggest + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class AttributeSuggestion( + @JsonProperty("attributeType") + val attributeType: String? = null, + + @JsonProperty("name") + val name: String? = null, + + @JsonProperty("value") + val value: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/suggest/QueryContext.kt b/src/main/kotlin/model/suggest/QueryContext.kt new file mode 100644 index 0000000..afb4745 --- /dev/null +++ b/src/main/kotlin/model/suggest/QueryContext.kt @@ -0,0 +1,9 @@ +package model.suggest + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class QueryContext( + @JsonProperty("originalQuery") + val originalQuery: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/suggest/QuerySuggestion.kt b/src/main/kotlin/model/suggest/QuerySuggestion.kt new file mode 100644 index 0000000..1fb848b --- /dev/null +++ b/src/main/kotlin/model/suggest/QuerySuggestion.kt @@ -0,0 +1,12 @@ +package model.suggest + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class QuerySuggestion( + @JsonProperty("displayText") + val displayText: String? = null, + + @JsonProperty("query") + val query: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/suggest/SearchSuggestion.kt b/src/main/kotlin/model/suggest/SearchSuggestion.kt new file mode 100644 index 0000000..48dc979 --- /dev/null +++ b/src/main/kotlin/model/suggest/SearchSuggestion.kt @@ -0,0 +1,25 @@ +package model.suggest + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse +import model.core.Variant + +data class SearchSuggestion( + @JsonProperty("pid") + val pid: String? = null, + + @JsonProperty("sale_price") + val salePrice: Double? = null, + + @JsonProperty("thumb_image") + val thumbImage: String? = null, + + @JsonProperty("title") + val title: String? = null, + + @JsonProperty("url") + val url: String? = null, + + @JsonProperty("variants") + val variants: List? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/suggest/SuggestResponse.kt b/src/main/kotlin/model/suggest/SuggestResponse.kt new file mode 100644 index 0000000..ae35fc3 --- /dev/null +++ b/src/main/kotlin/model/suggest/SuggestResponse.kt @@ -0,0 +1,12 @@ +package model.suggest + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class SuggestResponse( + @JsonProperty("queryContext") + val queryContext: QueryContext? = null, + + @JsonProperty("suggestionGroups") + val suggestionGroups: List? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/suggest/SuggestionGroup.kt b/src/main/kotlin/model/suggest/SuggestionGroup.kt new file mode 100644 index 0000000..560622c --- /dev/null +++ b/src/main/kotlin/model/suggest/SuggestionGroup.kt @@ -0,0 +1,21 @@ +package model.suggest + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class SuggestionGroup( + @JsonProperty("attributeSuggestions") + val attributeSuggestions: List? = null, + + @JsonProperty("catalogName") + val catalogName: String? = null, + + @JsonProperty("querySuggestions") + val querySuggestions: List? = null, + + @JsonProperty("searchSuggestions") + val searchSuggestions: List? = null, + + @JsonProperty("view") + val view: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/visualsearch/ImageUploadResponse.kt b/src/main/kotlin/model/visualsearch/ImageUploadResponse.kt new file mode 100644 index 0000000..6f6022a --- /dev/null +++ b/src/main/kotlin/model/visualsearch/ImageUploadResponse.kt @@ -0,0 +1,11 @@ +package model.visualsearch + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class ImageUploadResponse( + @JsonProperty("metadata") + var metadata: Metadata? = null, + @JsonProperty("response") + var response: Response? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/kotlin/model/visualsearch/Metadata.kt b/src/main/kotlin/model/visualsearch/Metadata.kt new file mode 100644 index 0000000..edaa24f --- /dev/null +++ b/src/main/kotlin/model/visualsearch/Metadata.kt @@ -0,0 +1,11 @@ +package model.visualsearch + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse +import model.rp.Widget +data class Metadata( + @JsonProperty("query") + var query: Query? = null, + @JsonProperty("widget") + var widget: Widget? = null +) : BaseResponse() diff --git a/src/main/kotlin/model/visualsearch/Query.kt b/src/main/kotlin/model/visualsearch/Query.kt new file mode 100644 index 0000000..c962790 --- /dev/null +++ b/src/main/kotlin/model/visualsearch/Query.kt @@ -0,0 +1,9 @@ +package model.visualsearch + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class Query( + @JsonProperty("image_id") + var imageId: String? = null +) : BaseResponse() diff --git a/src/main/kotlin/model/visualsearch/Response.kt b/src/main/kotlin/model/visualsearch/Response.kt new file mode 100644 index 0000000..d85d424 --- /dev/null +++ b/src/main/kotlin/model/visualsearch/Response.kt @@ -0,0 +1,9 @@ +package model.visualsearch + +import com.fasterxml.jackson.annotation.JsonProperty +import model.BaseResponse + +data class Response( + @JsonProperty("image_id") + var imageId: String? = null +) : BaseResponse() diff --git a/src/main/kotlin/model/widget/Attributes.kt b/src/main/kotlin/model/widget/Attributes.kt new file mode 100644 index 0000000..8a27a27 --- /dev/null +++ b/src/main/kotlin/model/widget/Attributes.kt @@ -0,0 +1,13 @@ +package model.widget + + +import com.fasterxml.jackson.annotation.JsonProperty + +data class Attributes( + @JsonProperty("brand") + val brand: String, + @JsonProperty("reviews_attr") + val reviewsAttr: String, + @JsonProperty("reviews_attr_count") + val reviewsAttrCount: String +) \ No newline at end of file diff --git a/src/main/kotlin/model/widget/MoreResult.kt b/src/main/kotlin/model/widget/MoreResult.kt new file mode 100644 index 0000000..0abfb9e --- /dev/null +++ b/src/main/kotlin/model/widget/MoreResult.kt @@ -0,0 +1,19 @@ +package model.widget + + +import com.fasterxml.jackson.annotation.JsonProperty + +data class MoreResult( + @JsonProperty("attributes") + val attributes: Attributes, + @JsonProperty("description") + val description: String, + @JsonProperty("image") + val image: String, + @JsonProperty("short-description") + val shortDescription: String, + @JsonProperty("title") + val title: String, + @JsonProperty("url") + val url: String +) \ No newline at end of file diff --git a/src/main/kotlin/model/widget/RelatedCategory.kt b/src/main/kotlin/model/widget/RelatedCategory.kt new file mode 100644 index 0000000..5324012 --- /dev/null +++ b/src/main/kotlin/model/widget/RelatedCategory.kt @@ -0,0 +1,11 @@ +package model.widget + + +import com.fasterxml.jackson.annotation.JsonProperty + +data class RelatedCategory( + @JsonProperty("anchor") + val anchor: String, + @JsonProperty("url") + val url: String +) \ No newline at end of file diff --git a/src/main/kotlin/model/widget/RelatedItem.kt b/src/main/kotlin/model/widget/RelatedItem.kt new file mode 100644 index 0000000..0e67a1c --- /dev/null +++ b/src/main/kotlin/model/widget/RelatedItem.kt @@ -0,0 +1,11 @@ +package model.widget + + +import com.fasterxml.jackson.annotation.JsonProperty + +data class RelatedItem( + @JsonProperty("anchor") + val anchor: String, + @JsonProperty("url") + val url: String +) \ No newline at end of file diff --git a/src/main/kotlin/model/widget/WidgetResponse.kt b/src/main/kotlin/model/widget/WidgetResponse.kt new file mode 100644 index 0000000..0e2ae2d --- /dev/null +++ b/src/main/kotlin/model/widget/WidgetResponse.kt @@ -0,0 +1,18 @@ +package model.widget + +import com.fasterxml.jackson.annotation.JsonProperty + +data class WidgetResponse( + @JsonProperty("br_iuid") + val brIuid: String, + @JsonProperty("br_related_rid") + val brRelatedRid: String, + @JsonProperty("br_related_rid_tag") + val brRelatedRidTag: String, + @JsonProperty("more-results") + val moreResults: List, + @JsonProperty("related-category") + val relatedCategory: List, + @JsonProperty("related-item") + val relatedItem: List +) \ No newline at end of file diff --git a/src/main/kotlin/network/ApiProcessor.kt b/src/main/kotlin/network/ApiProcessor.kt new file mode 100644 index 0000000..9286276 --- /dev/null +++ b/src/main/kotlin/network/ApiProcessor.kt @@ -0,0 +1,267 @@ +package network + +import BrApiRequest +import enums.ApiType +import enums.Env +import enums.ResponseType +import enums.VisitorType +import model.BrApiError +import model.visualsearch.ImageUploadResponse +import org.apache.hc.core5.net.URIBuilder +import java.io.InputStream +import java.net.URI +import java.net.URL + +/** + * Class for adding global parameters to the request, processing all types of API call and providing callback once API returns + */ +internal class ApiProcessor( + brApiRequest: BrApiRequest, + private val brHttpClient: BrHttpClient +) { + lateinit var brApiRequestData: BrApiRequest + + init { + this.brApiRequestData = brApiRequest + } + + private val CORE_API_PATH = "api/v1/core/" + private val SUGGEST_API_PATH = "api/v2/suggest" + private val SEO_WIDGET_API_PATH = "v3/fetch_widget" + private val WIDGET_API_PATH = "api/v2/widgets/" + private val SCEHEME = "https" + + /** + * Method to format Core API parameters, execute the API and invoke the callback with appropriate result + * @param params Map of request parameters to be sent with the request + * + * @return CoreResponse if API call success else return BrApiError object. + */ + fun processCoreApi(params: MutableMap): Any? { + var uriBuilder = FormatterUtils.mapToUriBuilderForApi(params) +// add global request parameters + uriBuilder = addGlobalQuery(uriBuilder) +// append base endpoint for API call + addBaseUrlForCoreApi(uriBuilder) + //perform API + return brHttpClient.invoke(uriBuilder.build(), ApiType.CORE, brApiRequestData) + } + + /** + * Method to format Thematic API parameters, execute the API and invoke the callback with appropriate result + * @param params Map of request parameters to be sent with the request + * @param responseType Response in JSON or HTML, defaulted to JSON + * + * @return CoreResponse or HTML if API call success else return BrApiError object. + */ + fun processThematicApi(params: MutableMap, responseType: ResponseType = ResponseType.JSON): Any? { + var uriBuilder = FormatterUtils.mapToUriBuilderForApi(params) +// add global request parameters + uriBuilder = addGlobalQuery(uriBuilder) +// append base endpoint for API call + addBaseUrlForCoreApi(uriBuilder) + //perform API + return brHttpClient.invoke(uriBuilder.build(), ApiType.CORE, brApiRequestData, responseType) + } + + /** + * Method to format Suggest API parameters, execute the API and invoke the callback with appropriate result + * @param params Map of request parameters to be sent with the request + * + * @return SuggestResponse if API call success else return BrApiError object. + */ + fun processSuggestApi( + params: MutableMap + ): Any? { + var uriBuilder = FormatterUtils.mapToUriBuilderForApi(params) +// add global request parameters + uriBuilder = addGlobalQuery(uriBuilder) +// append base endpoint for API call + addBaseUrlForSuggestApi(uriBuilder) + //perform API + return brHttpClient.invoke(uriBuilder.build(), ApiType.SUGGEST, brApiRequestData) + } + + /** + * Method to format SEO Widget API parameters, execute the API and invoke the callback with appropriate result + * @param params Map of request parameters to be sent with the request + * @param responseType Response in JSON or HTML, defaulted to JSON + * + * @return WidgetResponse or HTML if API call success else return BrApiError object. + */ + fun processWidgetApi(params: MutableMap, responseType: ResponseType = ResponseType.JSON): Any? { + var uriBuilder = FormatterUtils.mapToUriBuilderForApi(params) +// add acct_id manually here as the key name is different + uriBuilder.setParameter("acct_id", brApiRequestData.accountId) +// append base endpoint for API call + addBaseUrlForWidgetApi(uriBuilder) + +// set the output format key based on the response type required + if (responseType == ResponseType.JSON) { + uriBuilder.setParameter("output_format", "json") + } else { + uriBuilder.setParameter("output_format", "html") + } +// perform API + return brHttpClient.invoke(uriBuilder.build(), ApiType.WIDGET, brApiRequestData, responseType) + } + + /** + * Method to format Core API parameters, execute the API and invoke the callback with appropriate result + * @param params Map of request parameters to be sent with the request + * @param responseType Response in JSON or HTML, defaulted to JSON + * + * @return RecsAndPathwaysResponse if API call success else return BrApiError object. + */ + fun processRecsAndPathwaysApi( + widgetId: String, + widgetType: String, params: MutableMap + ): Any? { + var uriBuilder = FormatterUtils.mapToUriBuilderForApi(params) +// add global request parameters + uriBuilder = addGlobalQuery(uriBuilder) +// append base endpoint for API call + addBaseUrlForPathwaysApi(uriBuilder, widgetType, widgetId) + //perform API + return brHttpClient.invoke(uriBuilder.build(), ApiType.PATHWAYS, brApiRequestData) + } + + /** + * Method to call Image Upload API for visual search and invoke the callback with appropriate result + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * @param inputStream inputStream of the image + * @param filename file name of the image + */ + fun processVisualSearchUploadApi( + widgetId: String, + inputStream: InputStream, + filename: String + ): Any? { + var uriBuilder = URIBuilder() + // add global request parameters + uriBuilder = addGlobalQuery(uriBuilder) + // append base endpoint for Pixel call + addBaseUrlForPathwaysApi(uriBuilder, "visual/upload", widgetId) + //perform API + val uploadFile = + FileUpload(URL(uriBuilder.build().toString()), brApiRequestData.authKey ?: "") + uploadFile.addFilePart(inputStream, filename) + val result = uploadFile.upload() + return result + } + + /** + * Method to add global request parameters to Uri Builder + * @param uriBuilder The Uri.Builder where the global request parameters will be added in required format + */ + fun addGlobalQuery(uriBuilder: URIBuilder): URIBuilder { + uriBuilder.setParameter("account_id", brApiRequestData.accountId) + uriBuilder.setParameter("auth_key", brApiRequestData.authKey) + uriBuilder.setParameter("domain_key", brApiRequestData.domainKey) + uriBuilder.setParameter("request_id", FormatterUtils.generateRand()) + uriBuilder.setParameter( + "_br_uid_2", + FormatterUtils.formatCookieValue( + brApiRequestData.uuid ?: "", + brApiRequestData.visitorType ?: VisitorType.NEW_USER + ) + ) + if (!brApiRequestData.userId.isNullOrEmpty()) { + uriBuilder.setParameter("user_id", brApiRequestData.userId) + } + return uriBuilder + } + + /** + * Method to generate Base EndPoint Url for Stage Env + * + * @param uriBuilder The Uri.Builder the Base Url Endpoint will be set + * + */ + fun addBaseUrlForCoreApi(uriBuilder: URIBuilder) { + if (!brApiRequestData.baseUrl.isNullOrEmpty()) { + val uri = URI(brApiRequestData.baseUrl) + uriBuilder.scheme = uri.scheme + uriBuilder.host = uri.host + } else { + uriBuilder.scheme = SCEHEME + //check for env if stage or prod + when (brApiRequestData.environment) { + Env.STAGE -> uriBuilder.host = "staging-core.dxpapi.com" + Env.PROD -> uriBuilder.host = "core.dxpapi.com" + } + } + uriBuilder.path = CORE_API_PATH + + } + + /** + * Method to generate Base EndPoint Url for Stage Env + * + * @param uriBuilder The Uri.Builder the Base Url Endpoint will be set + * + */ + fun addBaseUrlForSuggestApi(uriBuilder: URIBuilder) { + if (!brApiRequestData.baseUrl.isNullOrEmpty()) { + val uri = URI(brApiRequestData.baseUrl) + uriBuilder.scheme = uri.scheme + uriBuilder.host = uri.host + } else { + uriBuilder.scheme = SCEHEME + //check for env if stage or prod + when (brApiRequestData.environment) { + Env.STAGE -> uriBuilder.host = "staging-suggest.dxpapi.com" + Env.PROD -> uriBuilder.host = "suggest.dxpapi.com" + } + } + uriBuilder.path = SUGGEST_API_PATH + } + + /** + * Method to generate Base EndPoint Url for SEO widget API + * + * @param uriBuilder The Uri.Builder the Base Url Endpoint will be set + */ + fun addBaseUrlForWidgetApi(uriBuilder: URIBuilder) { + if (!brApiRequestData.baseUrl.isNullOrEmpty()) { + val uri = URI(brApiRequestData.baseUrl) + uriBuilder.scheme = uri.scheme + uriBuilder.host = uri.host + } else { + uriBuilder.scheme = SCEHEME + //check for env if stage or prod + when (brApiRequestData.environment) { + Env.STAGE -> uriBuilder.host = "bsapi-test.brsrvr.com" + Env.PROD -> { + throw IllegalArgumentException("Base Url is not provided") + } + } + } + uriBuilder.path = SEO_WIDGET_API_PATH + } + + /** + * Method to generate Base EndPoint Url for Stage Env + * + * @param uriBuilder The Uri.Builder the Base Url Endpoint will be set + * @param widgetType Type of widget + * @param widgetId The ID of the widget, which can be found in the Widget Configurator in the Dashboard. + * + */ + fun addBaseUrlForPathwaysApi(uriBuilder: URIBuilder, widgetType: String, widgetId: String) { + if (!brApiRequestData.baseUrl.isNullOrEmpty()) { + val uri = URI(brApiRequestData.baseUrl) + uriBuilder.scheme = uri.scheme + uriBuilder.host = uri.host + } else { + uriBuilder.scheme = SCEHEME + //check for env if stage or prod + when (brApiRequestData.environment) { + Env.STAGE -> uriBuilder.host = "pathways-staging.dxpapi.com" + Env.PROD -> uriBuilder.host = "pathways.dxpapi.com" + } + } + uriBuilder.path = "$WIDGET_API_PATH$widgetType/$widgetId" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/network/BrHttpClient.kt b/src/main/kotlin/network/BrHttpClient.kt new file mode 100644 index 0000000..86c4885 --- /dev/null +++ b/src/main/kotlin/network/BrHttpClient.kt @@ -0,0 +1,114 @@ +package network + +import BrApiRequest +import com.fasterxml.jackson.databind.ObjectMapper +import enums.ApiType +import enums.ResponseType +import model.BrApiError +import model.core.CoreResponse +import model.rp.RecsAndPathwaysResponse +import model.rp.RpError +import model.suggest.SuggestResponse +import model.widget.WidgetResponse +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient +import org.apache.hc.core5.http.ClassicHttpRequest +import org.apache.hc.core5.http.io.entity.EntityUtils +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder +import java.net.URI +import org.slf4j.LoggerFactory + +/** + * Class to perform API calls + */ +internal class BrHttpClient(private val client: CloseableHttpClient) { + + private val logger = LoggerFactory.getLogger(BrHttpClient::class.java.name) + /** + * Method to HTTP call for all API + * @param url URL object containing request parameters + * @param type The type to identify of API call such as core, suggest. + * @param brApiRequest Object of all global paramters to be sent with the API + * @param responseType Response Type JSON or HTML + * + * @return Any response object, Based on ApiType and ResponseType + */ + fun invoke( + url: URI, + type: ApiType, + brApiRequest: BrApiRequest, + responseType: ResponseType = ResponseType.JSON + ): Any? { + + logger.trace("invoke() - ApiType: ${type}, responseType: $responseType") + logger.trace("url: ${url.toURL()}") + + val request: ClassicHttpRequest = ClassicRequestBuilder + .get() + .setUri(url) + .addHeader("Cache-Control", "no-cache") + .build() + + try { + client.execute(request).use { response -> + logger.info("API response code: ${response.code} ") + // check if response code is 200 + if (response.code in 200..299) { + val entity = response.entity + if (entity != null) { + // get response as a String + val result = EntityUtils.toString(entity) + logger.trace("Response: $result") + + return if (responseType == ResponseType.JSON) { + // if response type is JSON, do the jackson parsing + val responseMapper = ObjectMapper() + when (type) { + // if type matches do the parsing to respective model + ApiType.CORE -> responseMapper.readValue(result, CoreResponse::class.java) + ApiType.WIDGET -> responseMapper.readValue(result, WidgetResponse::class.java) + ApiType.SUGGEST -> responseMapper.readValue(result, SuggestResponse::class.java) + ApiType.PATHWAYS -> responseMapper.readValue( + result, + RecsAndPathwaysResponse::class.java + ) + } + } else { + // return the HTML response as String + result + } + } else { + logger.error("Error code: ${response.code}, Error Message: ${response.reasonPhrase}") + // return error with response code and reason + BrApiError(response.reasonPhrase, response.code) + } + } else { + // return error with response code and reason + val entity = response.entity + if (entity != null) { + // get response as a String + val result = EntityUtils.toString(entity) + logger.trace("Response: $result") + + return if (type == ApiType.PATHWAYS) { + val responseMapper = ObjectMapper() + val rpError = responseMapper.readValue(result, RpError::class.java) + BrApiError(rpError.detail ?: "Something went wrong", response.code) + } else { + logger.error("Error code: ${response.code}, Error Message: ${response.reasonPhrase}") + val responseMapper = ObjectMapper() + responseMapper.readValue(result, BrApiError::class.java) + } + + } else { + BrApiError(response.reasonPhrase, response.code) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + // return error with exception message + BrApiError(e.localizedMessage, 0) + } + return BrApiError("Something went wrong, please try again", -1) + } +} \ No newline at end of file diff --git a/src/main/kotlin/network/FileUpload.kt b/src/main/kotlin/network/FileUpload.kt new file mode 100644 index 0000000..62cac18 --- /dev/null +++ b/src/main/kotlin/network/FileUpload.kt @@ -0,0 +1,118 @@ +package network + +import com.fasterxml.jackson.databind.ObjectMapper +import model.BrApiError +import model.rp.RpError +import model.visualsearch.ImageUploadResponse +import org.slf4j.LoggerFactory +import java.io.* +import java.net.HttpURLConnection +import java.net.URL + +/** + * Class to perform File Upload API call + */ +internal class FileUpload(url: URL, authKey: String) { + companion object { + private val LINE_FEED = "\r\n" + private val maxBufferSize = 1024 * 1024 + private val charset = "UTF-8" + } + // creates a unique boundary based on time stamp + private val boundary: String = "===" + System.currentTimeMillis() + "===" + private val httpConnection: HttpURLConnection = url.openConnection() as HttpURLConnection + private val outputStream: OutputStream + private val writer: PrintWriter + + private val logger = LoggerFactory.getLogger(BrHttpClient::class.java.name) + + init { + httpConnection.setRequestProperty("Accept-Charset", "UTF-8") + httpConnection.setRequestProperty("Connection", "Keep-Alive") + httpConnection.setRequestProperty("Cache-Control", "no-cache") + httpConnection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary) + httpConnection.setRequestProperty("auth_key", authKey) + //TODO check +// httpConnection.setRequestProperty( +// "User-Agent", +// "Bloomreach/${BuildConfig.SDK_VERSION} " + System.getProperty("http.agent") +// ) + httpConnection.setChunkedStreamingMode(maxBufferSize) + httpConnection.doInput = true + httpConnection.doOutput = true // indicates POST method + httpConnection.useCaches = false + outputStream = httpConnection.outputStream + writer = PrintWriter(OutputStreamWriter(outputStream, charset), true) + } + + /** + * Method to add file part to the API call + * @param inputStream inputStream of the image + * @param fileName file name of the image + */ + @Throws(IOException::class) + fun addFilePart(inputStream: InputStream, fileName: String) { + logger.trace("addFilePart() - fileName: $fileName") + + writer.append("--").append(boundary).append(LINE_FEED) + writer.append("Content-Disposition: form-data; name=\"").append("image") + .append("\"; filename=\"").append(fileName).append("\"").append(LINE_FEED) + writer.append("Content-Type: ").append("image/jpeg").append(LINE_FEED) + writer.append(LINE_FEED) + writer.flush() + + inputStream.copyTo(outputStream, maxBufferSize) + + outputStream.flush() + inputStream.close() + writer.append(LINE_FEED) + writer.flush() + } + + /** + * Method to perform File Upload API + * @return Any response object CoreResponse if API call success else return BrApiError object + */ + @Throws(IOException::class) + fun upload() : Any { + logger.trace("upload()") + + writer.append(LINE_FEED).flush() + writer.append("--").append(boundary).append("--") + .append(LINE_FEED) + writer.close() + + try { + // checks server's status code first + val responseCode = httpConnection.responseCode + logger.info("API response code: $responseCode ") + // Log.i("Visual Search upload API CALL:", "responseCode: $responseCode") + if (responseCode in 200..299) { + val reader = BufferedReader(InputStreamReader(httpConnection + .inputStream)) + val response = reader.use(BufferedReader::readText) + logger.trace("Response: $response") + httpConnection.disconnect() + + val responseMapper = ObjectMapper() + return responseMapper.readValue(response, ImageUploadResponse::class.java) + } else { + if (httpConnection.errorStream != null) { + val result = httpConnection.errorStream.bufferedReader().use(BufferedReader::readText) + logger.trace("errorStream: $result") + + httpConnection.errorStream.close() + //covert error result to BrApiError object + val responseMapper = ObjectMapper() + val rpError = responseMapper.readValue(result, RpError::class.java) + return BrApiError(rpError.detail ?: "The server responded with an error while uploading the image", responseCode) + } + logger.error("Error code: ${responseCode}, Error Message: ${httpConnection.responseCode}") + return BrApiError("The server responded with an error while uploading the image", responseCode) + } + + } catch (exception: Exception) { + throw exception + } + } +} diff --git a/src/main/kotlin/network/FormatterUtils.kt b/src/main/kotlin/network/FormatterUtils.kt new file mode 100644 index 0000000..6f41162 --- /dev/null +++ b/src/main/kotlin/network/FormatterUtils.kt @@ -0,0 +1,57 @@ +package network + +import enums.VisitorType +import org.apache.hc.core5.net.URIBuilder +import java.net.URLDecoder +import kotlin.random.Random + +/** + * Utility class for performing string formatting operations + */ +internal object FormatterUtils { + /** + * Method to generate a random number + * + * @return random number in string + */ + fun generateRand(): String { + // generate random Long + return Random.nextLong().toString() + } + + /** + * Method to format Cookie2 value + * @param uuid Android random string + * @param hitcount ENUM VisitorType (The hitcount value should be 1 for a new visitor, or 2 for returning visitors.) + * + * @return cookie2 - String value in 'uid={{UUID}}:v=app:ts=0:hc={{hitcount}}' format + */ + fun formatCookieValue(uuid: String, hitcount: VisitorType): String { + // convert uid={{UUID}}:v=app:ts=0:hc={{hitcount}} + return "uid=$uuid:v=DiscoveryClientTools-Java-1.0:ts=0:hc=${hitcount.hitCount}" //DiscoveryClientTools-1.0 //TODO + } + + /** + * Method to convert Map to Uri.Builder for network calls + * + * @param queryMap array of the PixelBasketItem objects + * @return Uri.Builder - String value in required format + */ + fun mapToUriBuilderForApi(queryMap: MutableMap): URIBuilder { + val uriBuilder = URIBuilder() + queryMap.forEach { mapObject -> + if(mapObject.value is String) { + uriBuilder.setParameter(mapObject.key, getUrlDecodeString(mapObject.value as String)) + } else if(mapObject.value is List<*>) { // check for fq + for(value in mapObject.value as List<*>) { + uriBuilder.setParameter(mapObject.key, getUrlDecodeString(value as String)) + } + } + } + return uriBuilder + } + + private fun getUrlDecodeString(value: String): String { + return URLDecoder.decode(value, "UTF-8") + } +} \ No newline at end of file diff --git a/src/main/kotlin/request/AutosuggestRequest.kt b/src/main/kotlin/request/AutosuggestRequest.kt new file mode 100644 index 0000000..b6ff2e8 --- /dev/null +++ b/src/main/kotlin/request/AutosuggestRequest.kt @@ -0,0 +1,94 @@ +package request + +import java.util.stream.Collectors + +/** + * AutoSuggest Request Object class. Create the object of this class in order to + * send it with AutoSuggest API + */ +class AutosuggestRequest : RequestMap() { + // add hardcoded default parameters required for product search API + init { + setRequestType() + } + + /** + * Method to set hardcoded default parameters required for Auto Suggest API + * @return A reference request object + */ + private fun setRequestType(): AutosuggestRequest { + return set(ApiConstants.REQUEST_TYPE, ApiConstants.REQUEST_TYPE_SUGGEST) + } + + /** + * Method to set catalog views that you want to see in your suggestions. + * + * @param value catalogs views formatted in required format + * + * @return A reference request object + */ + fun catalogViews(value: String): AutosuggestRequest { + return set(ApiConstants.CATALOG_VIEWS, value) + } + + /** + * Method to set catalog views that you want to see in your suggestions. + * This method helps to format the catalogs views in required format + * + * @param values Map of catalog views attributes and its values + * + * @return A reference request object + */ + fun catalogViews(values: Map): AutosuggestRequest { + //converts to my_product_catalog:store1|recipe:daily + val catalogViewsStr = values.entries + .stream() + .map { e -> e.key + ":" + e.value } + .collect(Collectors.joining("|")) + return catalogViews(catalogViewsStr) + } + + /** + * Method to set search term for Search APIs + * + * @param q Partial search query that Autosuggest should operate on. + * + * @return A reference to the current Request object + */ + fun searchTerm(q: String): AutosuggestRequest { + return set(ApiConstants.SEARCH_TERM, q) + } + + /** + * The user agent of the BROWSER/MACHINE/DEVICE that's making the request. + * + * @param value user agent value + * + * @return A reference request object + */ + fun userAgent(value: String): AutosuggestRequest { + return set(ApiConstants.USER_AGENT, value) + } + + /** + * Method to set url + * + * @param value The title or name of the product. + * + * @return A reference to the current Request object + */ + fun url(value: String): AutosuggestRequest { + return set(ApiConstants.URL, value) + } + + /** + * Method to set user id of the customer + * + * @param value The universal customer ID of the user. + * + * @return A reference to the current Request object + */ + fun userId(value: String?): AutosuggestRequest { + return set(ApiConstants.USER_ID, value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/request/BestSellerRequest.kt b/src/main/kotlin/request/BestSellerRequest.kt new file mode 100644 index 0000000..cef1f37 --- /dev/null +++ b/src/main/kotlin/request/BestSellerRequest.kt @@ -0,0 +1,30 @@ +package request + +/** + * BestSeller Request Object class. Create the object of this class in order to + * send it with BestSeller API + */ +class BestSellerRequest : SearchRequest() { + + // add hardcoded default parameters required for BestSeller API + init { + setRequestType() + setSearchType() + } + + /** + * Method to set hardcoded default parameters required for BestSeller API + * @return A reference request object + */ + private fun setRequestType(): BestSellerRequest { + return set(ApiConstants.REQUEST_TYPE, ApiConstants.REQUEST_TYPE_SEARCH) + } + + /** + * Method to set hardcoded default parameters required for BestSeller API + * @return A reference request object + */ + private fun setSearchType(): BestSellerRequest { + return set(ApiConstants.SEARCH_TYPE, ApiConstants.SEARCH_TYPE_BESTSELLER) + } +} \ No newline at end of file diff --git a/src/main/kotlin/request/CategorySearchRequest.kt b/src/main/kotlin/request/CategorySearchRequest.kt new file mode 100644 index 0000000..eb852e1 --- /dev/null +++ b/src/main/kotlin/request/CategorySearchRequest.kt @@ -0,0 +1,35 @@ +package request + +class CategorySearchRequest() : SearchRequest() { + + // add hardcoded default parameters required for Category Search API + init { + setRequestType() + setSearchType() + } + + /** + * Method to set hardcoded default parameters required for Category Search API + * @return A reference request object + */ + private fun setRequestType(): CategorySearchRequest { + return set(ApiConstants.REQUEST_TYPE, ApiConstants.REQUEST_TYPE_SEARCH) + } + + /** + * Method to set hardcoded default parameters required for Category Search API + * @return A reference request object + */ + private fun setSearchType(): CategorySearchRequest { + return set(ApiConstants.SEARCH_TYPE, ApiConstants.SEARCH_TYPE_CATEGORY) + } + + /** + * Method to set hardcoded default parameters required for Category Search API + * @return A reference request object + */ + internal fun setDynamicCategory(): CategorySearchRequest { + return set(ApiConstants.CATEGORY_TYPE, ApiConstants.CATEGORY_TYPE_DYNAMIC) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/request/ContentSearchRequest.kt b/src/main/kotlin/request/ContentSearchRequest.kt new file mode 100644 index 0000000..22f151f --- /dev/null +++ b/src/main/kotlin/request/ContentSearchRequest.kt @@ -0,0 +1,48 @@ +package request + +/** + * Content Search Request Object class. Create the object of this class in order to + * send it with Content Search API + */ +class ContentSearchRequest() : SearchRequest() { + + // add hardcoded default parameters required for content search API + init { + setRequestType() + setSearchType() + } + + /** + * Method to set hardcoded default parameters required for content search API + * @return A reference request object + */ + private fun setRequestType(): ContentSearchRequest { + return set(ApiConstants.REQUEST_TYPE, ApiConstants.REQUEST_TYPE_SEARCH) + } + + /** + * Method to set hardcoded default parameters required for content search API + * @return A reference request object + */ + private fun setSearchType(): ContentSearchRequest { + return set(ApiConstants.SEARCH_TYPE, ApiConstants.SEARCH_TYPE_KEYWORD) + } + + /** + * Method to set catalog name. + * Named identifier of the catalog. A catalog is a grouping of items into a broader category + * such as blogs, videos, etc. A catalog is a representation + * of a group of items and must have a unique name, that is also unique to a domain + * (if you have multiple sites). + * + * @param value catalog name + * + * @return A reference request object + */ + fun catalogName(value: String): ContentSearchRequest { + if (value.isEmpty()) { + throw IllegalArgumentException("Catalog name cannot be empty") + } + return set(ApiConstants.CATALOG_NAME, value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/request/Operator.kt b/src/main/kotlin/request/Operator.kt new file mode 100644 index 0000000..e522fdc --- /dev/null +++ b/src/main/kotlin/request/Operator.kt @@ -0,0 +1,9 @@ +package request + +/** + * ENUM to specify AND / OR operator for applying filters/fq/efq + */ +enum class Operator { + OR, + AND +} \ No newline at end of file diff --git a/src/main/kotlin/request/ProductSearchRequest.kt b/src/main/kotlin/request/ProductSearchRequest.kt new file mode 100644 index 0000000..e7cf451 --- /dev/null +++ b/src/main/kotlin/request/ProductSearchRequest.kt @@ -0,0 +1,35 @@ +package request + +import ApiConstants.REQUEST_TYPE +import ApiConstants.REQUEST_TYPE_SEARCH +import ApiConstants.SEARCH_TYPE +import ApiConstants.SEARCH_TYPE_KEYWORD + +/** + * Product Search Request Object class. Create the object of this class in order to + * send it with Product Search API + */ +class ProductSearchRequest(): SearchRequest() { + + // add hardcoded default parameters required for product search API + init { + setRequestType() + setSearchType() + } + + /** + * Method to set hardcoded default parameters required for product search API + * @return A reference request object + */ + private fun setRequestType(): ProductSearchRequest { + return set(REQUEST_TYPE, REQUEST_TYPE_SEARCH) + } + + /** + * Method to set hardcoded default parameters required for product search API + * @return A reference request object + */ + private fun setSearchType(): ProductSearchRequest { + return set(SEARCH_TYPE, SEARCH_TYPE_KEYWORD) + } +} \ No newline at end of file diff --git a/src/main/kotlin/request/RequestMap.kt b/src/main/kotlin/request/RequestMap.kt new file mode 100644 index 0000000..fe96109 --- /dev/null +++ b/src/main/kotlin/request/RequestMap.kt @@ -0,0 +1,86 @@ +package request + +/** + * RequestMap class base class for all Request class, which stores the parameters in the map + */ +sealed class RequestMap { + private val requestMap = mutableMapOf() + + /** + * Method to set query parameter as key and value. + * If the key is already set, the value will get replaced + * + * @param key The name of the query parameter + * @param value The value that is used for the query parameter value. If the value is + *
null
the key will be removed + * + * @return A reference to the current Request object + */ + fun set(key: String, value: String?): T { + if (key.isEmpty()) { + throw IllegalArgumentException("Key cannot be empty") + } + + if (value.isNullOrEmpty()) { + requestMap.remove(key) + } else { + requestMap[key] = value + } + return this as T + } + + /** + * Method to add multiple query parameter for same key + * + * @param key The name of the query parameter + * @param value The value that is used for the query parameter value + * + * @return A reference to the current Request object + */ + fun add(key: String, value: String?): T { + if (key.isEmpty()) { + throw IllegalArgumentException("Key cannot be empty") + } + + if (!value.isNullOrEmpty()) { + if (requestMap.containsKey(key)) { + val mapValue = requestMap[key] + if (mapValue is ArrayList<*>) { + (mapValue as ArrayList).add(value) + requestMap[key] = mapValue + } else if (mapValue is String) { + val list = ArrayList() + list.add(mapValue as String) + list.add(value) + requestMap[key] = list + } + } else { + requestMap[key] = value + } + } else { + requestMap.remove(key) + } + return this as T + } + + + /** + * Method to set ref_url + * + * @param value The URL of the page or HTTP referrer where the request is started. + * + * @return A reference to the current Request object + */ + fun refUrl(value: String): T { + return set(ApiConstants.REF_URL, value) + } + + /** + * Method to get Request Map object + * + * @return A reference request map object + */ + fun getMap(): MutableMap { + return requestMap + } +} \ No newline at end of file diff --git a/src/main/kotlin/request/SearchRequest.kt b/src/main/kotlin/request/SearchRequest.kt new file mode 100644 index 0000000..4614cfd --- /dev/null +++ b/src/main/kotlin/request/SearchRequest.kt @@ -0,0 +1,473 @@ +package request + +import ApiConstants.DEFAULT_ROWS +import ApiConstants.DEFAULT_START +import ApiConstants.FL +import java.util.stream.Collectors + +/** + * This class is base class for Search APIs request it provides parameters to be sent with Search APIs + */ +sealed class SearchRequest() : RequestMap() { + + // add default parameters required for search API + init { + rows(DEFAULT_ROWS) + start(DEFAULT_START) + } + + /** + * Method to set rows Search APIs + * + * @param rows The number of matching items to return per results page in the API response. The maximum value is 200. + * + * @return A reference to the current Request object + */ + fun rows(rows: Int): T { + return set(ApiConstants.ROWS, rows.toString()) + } + + /** + * Method to set start Search APIs + * + * @param start The number of the first item on a page of results. For example, the first item on the first page is 0, making the start value also 0. + * + * @return A reference to the current Request object + */ + fun start(start: Int): T { + return set(ApiConstants.START, start.toString()) + } + + /** + * Method to set search term for Search APIs + * + * @param q Query key for Searching + * + * @return A reference to the current Request object + */ + fun searchTerm(q: String): T { + return set(ApiConstants.SEARCH_TERM, q) + } + + /** + * Method to set Field List for Search APIs + * + * @param value The comma separated attributes that you want returned in your API response, such as product IDs and prices. + * + * @return A reference to the current Request object + */ + fun fl(value: String): T { + if (value.isEmpty()) { + throw IllegalArgumentException("") + } + return set(FL, value) + } + + /** + * Method to set Field List for Search APIs + * + * @param values The attributes list that you want returned in your API response, such as product IDs and prices. + * + * @return A reference to the current Request object + */ + fun fl(values: List): T { + var flString = "" + if (values.isNullOrEmpty()) { + throw IllegalArgumentException() + } else if (values.size == 1) { + flString = values[0] + } else if (values.size > 1) { + flString = values.joinToString(",") + } + return fl(flString) + } + + /** + * Method to set Field List for Search APIs + * + * @param values The attributes array that you want returned in your API response, such as product IDs and prices. + * + * @return A reference to the current Request object + */ + fun fl(values: Array): T { + var flString = "" + if (values.isNullOrEmpty()) { + throw IllegalArgumentException() + } else if (values.size == 1) { + flString = values[0] + } else if (values.size > 1) { + flString = values.joinToString(",") + } + return fl(flString) + } + + /** + * Method to set sort parameter. You can alter the sequence in which products are + * displayed by passing the sort parameter. + * + * @param value Formatted value for sort parameter. 'price+asc' + * + * @return A reference to the current Request object + */ + fun sort(value: String?): T { + return set(ApiConstants.SORT, value) + } + + /** + * Method to set sort parameter. You can alter the sequence in which products are + * displayed by passing the sort parameter. + * + * @param sort sort object contains value for paramter on which sorting is to be done and SortOrder specifies the Order Asc or Desc + * + * @return A reference to the current Request object + */ + fun sort(sort: Sort): T { + return sort(sortString(sort)) + } + + /** + * Method to set sort parameter. You can alter the sequence in which products are + * displayed by passing the sort parameter. + * + * @param values list of sort objects contains value for paramter on which sorting is to be done and SortOrder specifies the Order Asc or Desc + * + * @return A reference to the current Request object + */ + fun sort(values: List?): T { + var sortString: String? = null + if (values.isNullOrEmpty()) { + sortString = null + } else if (values.size == 1) { + sortString = sortString(values[0]!!) + } else if (values.size > 1) { + sortString = sortString(values) + } + return sort(sortString) + } + + /** + * Method to format Sort object in required String format + * + * @param sort object + * + * @return Formatted string for sort parameter + */ + private fun sortString(sort: Sort):String { + return "${sort.value}+${sort.order.value}" + } + + /** + * Method to format List of Sort object in required String format + * + * @param sortList list of sort object + * + * @return Formatted string for sort parameter + */ + private fun sortString(sortList: List): String { + return sortList.stream() + .map { sort -> sortString(sort) } + .collect(Collectors.joining(",")) + } + + /** + * Method to set sort parameter. You can alter the sequence in which products are + * displayed by passing the sort parameter. + * + * @param values list of sort string + * + * @return A reference to the current Request object + */ + fun sort(values: Array?): T { + var sortString: String? = null + if (values.isNullOrEmpty()) { + sortString = null + } else if (values.size == 1) { + sortString = values[0] + } else if (values.size > 1) { + sortString = values.joinToString(",") + } + return sort(sortString) + } + + /** + * Method to set facetPrefix for Search APIs. + * The facet.prefix parameter limits faceting to terms that start with the specified string prefix. + * + * @param facetName The name of the facet + * @param prefixValue value for facet prefix + * + * @return A reference to the current Request object + */ + fun facetPrefix(facetName: String, prefixValue: String): T { + val key = "f.${facetName}.facet.prefix" + return set(key, prefixValue) + } + + /** + * Method to set facetPrefix for Widget APIs + * The facet.prefix parameter limits faceting to terms that start with the specified string prefix. + * + * @param facetName The name of the facet + * @param prefixValue value for facet prefix + * + * @return A reference to the current Request object + */ + fun facetPrefixWidget(facetName: String, prefixValue: String): T { + return set("facet.prefix", "$facetName:$prefixValue") + } + + /** + * Method to set fq + * The fq parameter is an optional parameter that you can add to an API request to filter the results. + * + * @param value The formatted value to be passed to fq parameter + * + * @return A reference to the current Request object + */ + fun fq(value: String): T { + return add(ApiConstants.FQ, value) + } + + /** + * Method to set fq with attribute and its single value + * The fq parameter is an optional parameter that you can add to an API request to filter the results. + * + * @param attribute The attribute for fq + * @param value The value of the attribute + * + * @return A reference to the current Request object + */ + fun fq(attribute: String, value: String): T { + val fqValue = "$attribute:\"${value}\"" + return fq(fqValue) + } + + /** + * Method to set fq with attribute and its multiple value + * The fq parameter is an optional parameter that you can add to an API request to filter the results. + * + * @param attribute The attribute for fq + * @param values The list of multiple possible values for given attribute. + * + * @return A reference to the current Request object + */ + fun fq(attribute: String, values: List): T { + var str = "" + if (values.size > 1) { + for ((index, value) in values.withIndex()) { + str += "\"${value}\"" + if (index != values.size - 1) { + str += " OR " + } + } + } else { + str += values[0] + } + return fq(attribute, str) + } + + /** + * Method to set stats.field + * The stats.field allows you to display the maximum and minimum values of any numeric field in your data set for a user query. + * + * @param value The formatted stats.field value + * + * @return A reference to the current Request object + */ + fun statsField(value: String?): T { + return set(ApiConstants.STATS_FIELD, value) + } + + fun statsField(values: List): T { + var sfString: String? = null + if (values.isNullOrEmpty()) { + sfString = null + } else if (values.size == 1) { + sfString = values[0] + } else if (!values.isNullOrEmpty() && values.size > 1) { + sfString = values.joinToString(",") + } + return statsField(sfString) + } + + fun statsField(values: Array): T { + var sfString: String? = null + if (values.isNullOrEmpty()) { + sfString = null + } else if (values.size == 1) { + sfString = values[0] + } else if (!values.isNullOrEmpty() && values.size > 1) { + sfString = values.joinToString(",") + } + return statsField(sfString) + } + + fun efq(value: String): T { + return set(ApiConstants.EFQ, value) + } + + fun efq(attribute: String, value: String): T { + return efq("$attribute:(\"${value}\")") + } + + fun efq(attribute: String, value: String, isNot: Boolean): T { + return if(isNot) { + efq("-$attribute:(\"${value}\")") + } else { + efq("$attribute:(\"${value}\")") + } + } + + /** + * Method to set efq with attribute and its multiple values + * + * @param attribute The attribute for efq + * @param values The list of multiple possible values for given attribute. + * @param operator 'AND' or 'OR' operator for values + * + * @return A reference to the current Request object + */ + fun efq(attribute: String, values: List, operator: Operator): T { + var str = "" + if (values.size > 1) { + //attribute:("value 1" OR "value 2") + for ((index, value) in values.withIndex()) { + str += "\"${value}\"" //TODO + if (index != values.size - 1) { + if (operator == Operator.OR) { + str += " OR " + } else if (operator == Operator.AND) { + str += " AND " + } + } + } + } else { + str += values[0] + } + return efq(attribute, str) + } + + /** + * Method to set efq with multiple attribute and values + * + * @param values The map of multiple possible attributes and its values. + * @param operator 'AND' or 'OR' operator for attributes + * + * @return A reference to the current Request object + */ + fun efq(values: Map, operator: Operator): T { + var formattedStr = "" + //attribute1:("value") OR attribute2:("value") + if (values.size > 1) { + formattedStr = if (operator == Operator.OR) { + values.entries.stream() + .map { e -> "${e.key}:(\"${e.value}\")" } + .collect(Collectors.joining(" OR ")) + } else { + values.entries.stream() + .map { e -> "${e.key}:(\"${e.value}\")" } + .collect(Collectors.joining(" AND ")) + } + } + return efq(formattedStr) + } + + /** + * Method to set facet.range parameter + * Use the facet.range parameter to include ranged facets + * + * @param value value for the facet range + * + * @return A reference to the current Request object + */ + fun facetRange(value: String?): T { + return set(ApiConstants.FACET_RANGE, value) + } + + /** + * Method to set list of facet.range parameter + * Use the facet.range parameter to include ranged facets + * + * @param values list for the facet range + * + * @return A reference to the current Request object + */ + fun facetRange(values: List): T { + var frString: String? = null + if (values.isNullOrEmpty()) { + frString = null + } else if (values.size == 1) { + frString = values[0] + } else if (values.size > 1) { + frString = values.joinToString(",") + } + return facetRange(frString) + } + + /** + * BOPIS-specific parameter to specify the end-customer's latitude-longitude. + * + * @param value value for lat long in format 'lat,long' + * + * @return A reference to the current Request object + */ + fun latLong(value: String?): T { + return set(ApiConstants.LAT_LONG, value) + } + + /** + * Method to set View Id + * + * @param value A unique identifier for a specific view of your product catalog. + * + * @return A reference to the current Request object + */ + fun viewId(value: String?): T { + return set(ApiConstants.VIEW_ID, value) + } + + /** + * Method to set user id of the customer + * + * @param value The universal customer ID of the user. + * + * @return A reference to the current Request object + */ + fun userId(value: String?): T { + return set(ApiConstants.USER_ID, value) + } + + /** + * Method to set widget Id, The widget_id provided in the Dashboard for the Dynamic Widgets + * feature, which is used to provided curated results. + * + * @param value value for widget id + * + * @return A reference to the current Request object + */ + fun widgetId(value: String?): T { + return set(ApiConstants.WIDGET_ID, value) + } + + /** + * Method to set title + * + * @param value The title or name of the product. + * + * @return A reference to the current Request object + */ + fun title(value: String?): T { + return set(ApiConstants.TITLE, value) + } + + /** + * Method to set url + * + * @param value The title or name of the product. + * + * @return A reference to the current Request object + */ + fun url(value: String): T { + return set(ApiConstants.URL, value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/request/SeoWidgetRequest.kt b/src/main/kotlin/request/SeoWidgetRequest.kt new file mode 100644 index 0000000..116b476 --- /dev/null +++ b/src/main/kotlin/request/SeoWidgetRequest.kt @@ -0,0 +1,86 @@ +package request + +/** + * SeoWidget API Request Object class. Create the object of this class in order to + * send it with SeoWidget API + */ +class SeoWidgetRequest : RequestMap() { + + /** + * Method to set Acct Auth + * + * @param value The Bloomreach provided authentication key for your account ID. + * + * @return A reference to the current Request object + */ + fun acctAuth(value: String): SeoWidgetRequest { + return set(ApiConstants.ACCT_AUTH, value) + } + + /** + * Method to set ptype + * + * @param value The page type. + * + * @return A reference to the current Request object + */ + fun pType(value: String): SeoWidgetRequest { + return set(ApiConstants.P_TYPE, value) + } + + /** + * Method to set prod_id + * + * @param value The unique identifier for the product in your product catalog or database. This parameter affects product pages only. Not needed for category requests. + * + * @return A reference to the current Request object + */ + fun prodId(value: String): SeoWidgetRequest { + return set(ApiConstants.PROD_ID, value) + } + + /** + * Method to set prod_name + * + * @param value The name of the product in your product catalog or database. Not needed for category requests. + * + * @return A reference to the current Request object + */ + fun prodName(value: String): SeoWidgetRequest { + return set(ApiConstants.PROD_NAME, value) + } + + /** + * Method to set pstatus + * + * @param value The status of a product on the page. This parameter affects product pages only. + * + * @return A reference to the current Request object + */ + fun pStatus(value: String): SeoWidgetRequest { + return set(ApiConstants.P_STATUS, value) + } + + /** + * Method to set url + * + * @param value The URL for the webpage that is sending the request. + * + * @return A reference to the current Request object + */ + fun url(value: String): SeoWidgetRequest { + return set(ApiConstants.URL, value) + } + + /** + * The user agent of the BROWSER/MACHINE/DEVICE that's making the request. + * + * @param value user agent value + * + * @return A reference request object + */ + fun userAgent(value: String): SeoWidgetRequest { + return set(ApiConstants.USER_AGENT, value) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/request/Sort.kt b/src/main/kotlin/request/Sort.kt new file mode 100644 index 0000000..fd02fb2 --- /dev/null +++ b/src/main/kotlin/request/Sort.kt @@ -0,0 +1,7 @@ +package request + + +/** + * Data class for Sort object + */ +data class Sort(val value: String, val order: SortOrder) \ No newline at end of file diff --git a/src/main/kotlin/request/SortOrder.kt b/src/main/kotlin/request/SortOrder.kt new file mode 100644 index 0000000..9c72349 --- /dev/null +++ b/src/main/kotlin/request/SortOrder.kt @@ -0,0 +1,9 @@ +package request + +/** + * ENUM to specify Sort Order Ascending or descending for Search Request + */ +enum class SortOrder(val value: String) { + ASCENDING("asc"), + DESCENDING("desc") +} \ No newline at end of file diff --git a/src/main/kotlin/request/ThematicRequest.kt b/src/main/kotlin/request/ThematicRequest.kt new file mode 100644 index 0000000..7e76710 --- /dev/null +++ b/src/main/kotlin/request/ThematicRequest.kt @@ -0,0 +1,270 @@ +package request + +import ApiConstants.DEFAULT_ROWS +import ApiConstants.DEFAULT_START +import ApiConstants.FL +import ApiConstants.REQUEST_TYPE +import ApiConstants.REQUEST_TYPE_SEARCH +import ApiConstants.SEARCH_TYPE +import ApiConstants.SEARCH_TYPE_KEYWORD + +class ThematicRequest : RequestMap() { + + // add default parameters required for search API + init { + rows(DEFAULT_ROWS) + start(DEFAULT_START) + setRequestType() + setSearchType() + wt("json") + } + + /** + * Method to set rows Search APIs + * + * @param rows The number of matching items to return per results page in the API response. The maximum value is 200. + * + * @return A reference to the current Request object + */ + fun rows(rows: Int): ThematicRequest { + return set(ApiConstants.ROWS, rows.toString()) + } + + /** + * Method to set start Search APIs + * + * @param start The number of the first item on a page of results. For example, the first item on the first page is 0, making the start value also 0. + * + * @return A reference to the current Request object + */ + fun start(start: Int): ThematicRequest { + return set(ApiConstants.START, start.toString()) + } + + /** + * Method to set hardcoded default parameters required for product search API + * @return A reference request object + */ + private fun setRequestType(): ThematicRequest { + return set(REQUEST_TYPE, REQUEST_TYPE_SEARCH) + } + + /** + * Method to set hardcoded default parameters required for product search API + * @return A reference request object + */ + private fun setSearchType(): ThematicRequest { + return set(SEARCH_TYPE, SEARCH_TYPE_KEYWORD) + } + + /** + * Method to set search term for Search APIs + * + * @param q Query key for Searching + * + * @return A reference to the current Request object + */ + fun searchTerm(q: String): ThematicRequest { + return set(ApiConstants.SEARCH_TERM, q) + } + + /** + * Method to set Field List for Search APIs + * + * @param value The comma separated attributes that you want returned in your API response, such as product IDs and prices. + * + * @return A reference to the current Request object + */ + fun fl(value: String): ThematicRequest { + if (value.isEmpty()) { + throw IllegalArgumentException("") + } + return set(FL, value) + } + + /** + * Method to set Field List for Search APIs + * + * @param values The attributes list that you want returned in your API response, such as product IDs and prices. + * + * @return A reference to the current Request object + */ + fun fl(values: List): ThematicRequest { + var flString = "" + if (values.isNullOrEmpty()) { + throw IllegalArgumentException() + } else if (values.size == 1) { + flString = values[0] + } else if (values.size > 1) { + flString = values.joinToString(",") + } + return fl(flString) + } + + /** + * Method to set Field List for Search APIs + * + * @param values The attributes array that you want returned in your API response, such as product IDs and prices. + * + * @return A reference to the current Request object + */ + fun fl(values: Array): ThematicRequest { + var flString = "" + if (values.isNullOrEmpty()) { + throw IllegalArgumentException() + } else if (values.size == 1) { + flString = values[0] + } else if (values.size > 1) { + flString = values.joinToString(",") + } + return fl(flString) + } + + /** + * Method to set url + * + * @param value url of the page + * + * @return A reference to the current Request object + */ + fun url(value: String): ThematicRequest { + return set(ApiConstants.URL, value) + } + + /** + * Method to set wt + * + * @param value JSON or HTML + * + * @return A reference to the current Request object + */ + fun wt(value: String): ThematicRequest { + return set("wt", value) + } + + /** + * Method to set debug + * + * @param value + * + * @return A reference to the current Request object + */ + fun debug(value: Boolean): ThematicRequest { + return set("wt", value.toString()) + } + + /** + * Method to set facet.range parameter + * Use the facet.range parameter to include ranged facets + * + * @param value value for the facet range + * + * @return A reference to the current Request object + */ + fun facetRange(value: String?): ThematicRequest { + return set(ApiConstants.FACET_RANGE, value) + } + + /** + * Method to set list of facet.range parameter + * Use the facet.range parameter to include ranged facets + * + * @param values list for the facet range + * + * @return A reference to the current Request object + */ + fun facetRange(values: List): ThematicRequest { + var frString: String? = null + if (values.isNullOrEmpty()) { + frString = null + } else if (values.size == 1) { + frString = values[0] + } else if (values.size > 1) { + frString = values.joinToString(",") + } + return facetRange(frString) + } + + /** + * Method to set fq + * The fq parameter is an optional parameter that you can add to an API request to filter the results. + * + * @param value The formatted value to be passed to fq parameter + * + * @return A reference to the current Request object + */ + fun fq(value: String): ThematicRequest { + return add(ApiConstants.FQ, value) + } + + /** + * Method to set fq with attribute and its single value + * The fq parameter is an optional parameter that you can add to an API request to filter the results. + * + * @param attribute The attribute for fq + * @param value The value of the attribute + * + * @return A reference to the current Request object + */ + fun fq(attribute: String, value: String): ThematicRequest { + val fqValue = "$attribute:\"${value}\"" + return fq(fqValue) + } + + /** + * Method to set fq with attribute and its multiple value + * The fq parameter is an optional parameter that you can add to an API request to filter the results. + * + * @param attribute The attribute for fq + * @param values The list of multiple possible values for given attribute. + * + * @return A reference to the current Request object + */ + fun fq(attribute: String, values: List): ThematicRequest { + var str = "" + if (values.size > 1) { + for ((index, value) in values.withIndex()) { + str += "\"${value}\"" + if (index != values.size - 1) { + str += " OR " + } + } + } else { + str += values[0] + } + return fq(attribute, str) + } + + /** + * The user agent of the BROWSER/MACHINE/DEVICE that's making the request. + * + * @param value user agent value + * + * @return A reference request object + */ + fun userAgent(value: String): ThematicRequest { + return set(ApiConstants.USER_AGENT, value) + } + + /** + * Method to set user id of the customer + * + * @param value The universal customer ID of the user. + * + * @return A reference to the current Request object + */ + fun userId(value: String): ThematicRequest { + return set(ApiConstants.USER_ID, value) + } + + /** + * Method to set userIp + * + * @param value The IP address of the browser that's loading the thematic page. + * + * @return A reference to the current Request object + */ + fun userIp(value: String): ThematicRequest { + return set(ApiConstants.USER_IP, value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/request/WidgetRequest.kt b/src/main/kotlin/request/WidgetRequest.kt new file mode 100644 index 0000000..2ddb6f0 --- /dev/null +++ b/src/main/kotlin/request/WidgetRequest.kt @@ -0,0 +1,413 @@ +package request + +import ApiConstants.DEFAULT_FACET_FLAG + +/** + * Widget API Request Object class. Create the object of this class in order to + * send it with Recommendation Search API + */ +class WidgetRequest() : RequestMap() { + + // add default parameters required for search API + init { + facet(DEFAULT_FACET_FLAG) + } + + /** + * Method to set rows + * + * @param rows The number of matching items to return per results page in the API response. The maximum value is 200. + * + * @return A reference to the current Request object + */ + fun rows(rows: Int): WidgetRequest { + return set(ApiConstants.ROWS, rows.toString()) + } + + /** + * Method to set start + * + * @param start The number of the first item on a page of results. For example, the first item on the first page is 0, making the start value also 0. + * + * @return A reference to the current Request object + */ + fun start(start: Int): WidgetRequest { + return set(ApiConstants.START, start.toString()) + } + + /** + * Method to set itemIds for Item Based Widget API + * + * @param value Specifies access to a particular set of items. For product catalog, this would be the PID(s). + * + * @return A reference of Request object + */ + fun itemIds(value: String): WidgetRequest { + if (value.isEmpty()) { + throw IllegalArgumentException("ItemIds can't be empty") + } + return set(ApiConstants.ITEM_IDS, value) + } + + /** + * Method to set itemIds for Item Based Widget API + * + * @param values Array of access to a particular set of items. For product catalog, this would be the PID(s). + * + * @return A reference to the current Request object + */ + fun itemIds(values: Array): WidgetRequest { + var itemIds = "" + if (values.isNullOrEmpty()) { + throw IllegalArgumentException() + } else if (values.size == 1) { + itemIds = values[0] + } else if (values.size > 1) { + itemIds = values.joinToString(",") + } + return itemIds(itemIds) + } + + /** + * Method to set itemIds for Item Based Widget API + * + * @param values List of access to a particular set of items. For product catalog, this would be the PID(s). + * + * @return A reference to the current Request object + */ + fun itemIds(values: List): WidgetRequest { + var itemIds = "" + if (values.isNullOrEmpty()) { + throw IllegalArgumentException("ItemIds can't be empty") + } else if (values.size == 1) { + itemIds = values[0] + } else if (values.size > 1) { + itemIds = values.joinToString(",") + } + return itemIds(itemIds) + } + + /** + * Method to set cat Id for Category Based Widget API + * + * @param value Your site's category ID. + * + * @return A reference of Request object + */ + fun catId(value: String): WidgetRequest { + if (value.isEmpty()) { + throw IllegalArgumentException("Category Id can't be empty") + } + return set(ApiConstants.CAT_ID, value) + } + + /** + * Method to set search query for Keyword and Personalization Based Widget API + * + * @param value search query. + * + * @return A reference of Request object + */ + fun query(value: String): WidgetRequest { + if (value.isEmpty()) { + throw IllegalArgumentException("Search query Id can't be empty") + } + return set(ApiConstants.QUERY, value) + } + + /** + * Method to set user id required for Personalization-based Recommendation widgets + * + * @param value The universal customer ID of the user. + * + * @return A reference of Request object + */ + fun userId(value: String): WidgetRequest { + if (value.isEmpty()) { + throw IllegalArgumentException("User Id can't be empty") + } + return set(ApiConstants.USER_ID, value) + } + + /** + * Method to set context id Item-based Recommendation widget API + * + * @param value takes a single product ID for producing Context-based merchandising results for the widget. + * + * @return A reference of Request object + */ + fun contextId(value: String?): WidgetRequest { + return set(ApiConstants.CONTEXT_ID, value) + } + + /** + * Method to set fields Recommendation widget APIs + * + * @param value A formatted comma-separated list of fields to be sent in the request. + * + * @return A reference of Request object + */ + fun fields(value: String?): WidgetRequest { + return set(ApiConstants.FIELDS, value) + } + + /** + * Method to set fields Recommendation widget API + * + * @param values List of fields to be sent in the request. + * + * @return A reference of Request object + */ + fun fields(values: List?): WidgetRequest { + var fieldString: String? = null + if (values.isNullOrEmpty()) { + fieldString = null + } else if (values.size == 1) { + fieldString = values[0] + } else if (values.size > 1) { + fieldString = values.joinToString(",") + } + return fields(fieldString) + } + + /** + * Method to set fields Recommendation widget API + * + * @param values Array of fields to be sent in the request. + * + * @return A reference of Request object + */ + fun fields(values: Array?): WidgetRequest { + var fieldString: String? = null + if (values.isNullOrEmpty()) { + fieldString = null + } else if (values.size == 1) { + fieldString = values[0] + } else if (values.size > 1) { + fieldString = values.joinToString(",") + } + return fields(fieldString) + } + + /** + * Method to set filter facet for Keyword and Category Recommendation widget APIs + * + * @param value A formatted value to be sent in the request. + * + * @return A reference of Request object + */ + fun filterFacet(value: String): WidgetRequest { + return add(ApiConstants.FILTER_FACET, value) + } + + /** + * Method to set filter facet for Keyword and Category Recommendation widget APIs + * + * @param attribute filter facet attribute + * @param value value for the given attribute + * + * @return A reference of Request object + */ + fun filterFacet(attribute: String, value: String): WidgetRequest { + return filterFacet("$attribute:\"${value}\"") + } + + /** + * Method to set filter facet for Keyword and Category Recommendation widget APIs + * + * @param attribute filter facet attribute + * @param values The list of multiple possible values for given attribute. + * @param operator 'AND' or 'OR' operator for values + * + * @return A reference of Request object + */ + fun filterFacet(attribute: String, values: List, operator: Operator): WidgetRequest { + var str = "$attribute:" + if (values.size > 1) { + //attribute:"value 1" OR "value 2" + for ((index, value) in values.withIndex()) { + str += "\"${value}\"" + if (index != values.size - 1) { + if (operator == Operator.OR) { + str += " OR " + } else if (operator == Operator.AND) { + str += " AND " + } + } + } + return filterFacet(str) + } else { + return filterFacet(attribute, values[0]) + } + } + + /** + * Method to View Id + * + * @param value A unique identifier for a specific view of your product catalog. + * + * @return A reference of Request object + */ + fun viewId(value: String?): WidgetRequest { + return set(ApiConstants.VIEW_ID, value) + } + + /** + * Method to apply facet + * + * @param value Boolean value to enable or disable facet filtering + * + * @return A reference of Request object + */ + fun facet(value: Boolean): WidgetRequest { + return set(ApiConstants.FACET, value.toString()) + } + + /** + * Method to set filter for Recommendation widget APIs + * + * @param value The formatted value for given + * + * @return A reference of Request object + */ + fun filter(value: String): WidgetRequest { + return add(ApiConstants.FILTER, value) + } + + /** + * Method to set filter for Recommendation widget APIs + * + * @param attribute filter attribute value + * @param value The formatted value for given attribute + * @param isNot if '-' is needed at start pass value as true. Default set to false, + * + * @return A reference of Request object + */ + fun filter(attribute: String, value: String, isNot: Boolean = false): WidgetRequest { + if (isNot) { + return filter("-($attribute:(\"${value}\"))") + } else { + return filter("($attribute:(\"${value}\"))") + } + } + + /** + * Method to set filter with range for Recommendation widget APIs + * + * @param attribute filter attribute value + * @param startRange The start value for range filter + * @param endRange The end value for range filter + * @param isNot if '-' is needed at start pass value as true. Default set to false, + * + * @return A reference of Request object + */ + fun filter( + attribute: String, + startRange: String, + endRange: String, + isNot: Boolean = false + ): WidgetRequest { + if (isNot) { + return filter("-($attribute:[\"${startRange}\" TO \"${endRange}\"])") + } else { + return filter("($attribute:[\"${startRange}\" TO \"${endRange}\"])") + } + } + + /** + * Method to set filter with range for Recommendation widget APIs + * + * @param attribute filter attribute value + * @param startRange The start value for range filter + * @param endRange The end value for range filter + * @param isNot if '-' is needed at start pass value as true. Default set to false, + * + * @return A reference of Request object + */ + fun filter( + attribute: String, + startRange: Int, + endRange: Int, + isNot: Boolean = false + ): WidgetRequest { + return filter(attribute, startRange.toString(), endRange.toString(), isNot) + } + + /** + * Method to set filter with range for Recommendation widget APIs + * + * @param attribute filter attribute value + * @param range The range(0..100) value for range filter + * @param isNot if '-' is needed at start pass value as true. Default set to false, + * + * @return A reference of Request object + */ + fun filter(attribute: String, range: IntRange, isNot: Boolean = false): WidgetRequest { + return filter(attribute, range.first.toString(), range.last.toString(), isNot) + } + + /** + * Method to set filter with attribute and its multiple values + * + * @param attribute The attribute for filter + * @param values The list of multiple possible values for given attribute. + * @param operator 'AND' or 'OR' operator for values + * + * @return A reference to the current Request object + */ + fun filter(attribute: String, values: List, operator: Operator): WidgetRequest { + var str = "" + if (values.size > 1) { + //attribute:("value 1" OR "value 2") + for ((index, value) in values.withIndex()) { + str += "\"${value}\"" + if (index != values.size - 1) { + if (operator == Operator.OR) { + str += " OR " + } else if (operator == Operator.AND) { + str += " AND " + } + } + } + } else { + str += "\"${values[0]}\"" + } + + return add(ApiConstants.FILTER, "$attribute:($str)") + } + + /** + * Method to set facetPrefix for Widget APIs + * The facet.prefix parameter limits faceting to terms that start with the specified string prefix. + * + * @param facetName The name of the facet + * @param prefixValue value for facet prefix + * + * @return A reference to the current Request object + */ + fun facetPrefix(facetName: String, prefixValue: String): WidgetRequest { + return set("facet.prefix", "$facetName:$prefixValue") + } + + /** + * Method to set url + * + * @param value The absolute URL of the page where the request is initiated. Do not use a relative URL. + * + * @return A reference to the current Request object + */ + fun url(value: String): WidgetRequest { + return set(ApiConstants.URL, value) + } + + /** + * Method to set the image ID for Visual Search + * + * @param value image id + * + * @return A reference to the current Request object + */ + fun imageId(value: String): WidgetRequest { + return set(ApiConstants.IMAGE_ID, value) + } +} \ No newline at end of file