diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..ca33bb1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 24 + targetSdk 32 + versionCode 1 + versionName "1.0.5" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + buildTypes { + debug { + buildConfigField "String", 'SDK_VERSION', '"1.0.5"' + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + release { + buildConfigField "String", 'SDK_VERSION', '"1.0.5"' + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.5.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' +// implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5' + + testImplementation 'junit:junit:4.13.2' + testImplementation "org.robolectric:robolectric:4.+" + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.6.0' +} \ No newline at end of file diff --git a/consumer-rules.pro b/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/proguard-rules.pro b/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/src/androidTest/java/com/bloomreach/discovery/pixel/ExampleInstrumentedTest.kt b/src/androidTest/java/com/bloomreach/discovery/pixel/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..c47ac74 --- /dev/null +++ b/src/androidTest/java/com/bloomreach/discovery/pixel/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.bloomreach.discovery.pixel + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.bloomreach.discovery.pixel.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2662982 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/ApiConstants.kt b/src/main/java/com/bloomreach/discovery/api/ApiConstants.kt new file mode 100644 index 0000000..615c181 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/ApiConstants.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api + +/** + * API module constants including request parameters + */ +internal object ApiConstants { + + const val REQUEST_TYPE = "request_type" + const val SEARCH_TYPE = "search_type" + + const val REQUEST_TYPE_SEARCH = "search" + const val REQUEST_TYPE_SUGGEST = "suggest" + + const val SEARCH_TYPE_KEYWORD = "keyword" + const val SEARCH_TYPE_CATEGORY = "category" + 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 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 + + +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/BrApi.kt b/src/main/java/com/bloomreach/discovery/api/BrApi.kt new file mode 100644 index 0000000..ee47504 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/BrApi.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api + +import com.bloomreach.discovery.api.listener.BrApiCompletionListener +import com.bloomreach.discovery.api.network.ApiProcessor +import com.bloomreach.discovery.api.request.* + +/** + * BrApi Singleton class holds method to initiate BrApiRequest object and API calls methods + */ +object BrApi { + private val TAG: String = BrApi.javaClass.simpleName + private val apiProcessor = ApiProcessor() + lateinit var brApiRequest: BrApiRequest + + /** + * Initialise BrApi class with BrApiRequest object + * @param brApiRequest BrApiRequest object defined for initialisation + */ + public fun init(brApiRequest: BrApiRequest) { + BrApi.brApiRequest = brApiRequest + } + + /** + * Method for calling Product Search API request + * @param productSearchRequest Request Object required for Product Search API + * @param brApiCompletionListener Callback listener + */ + fun productSearchApi(productSearchRequest: ProductSearchRequest, brApiCompletionListener: BrApiCompletionListener) { + apiProcessor.processCoreApi(productSearchRequest.getMap(), brApiCompletionListener) + } + + /** + * Method for calling Category Search API request + * @param categorySearchRequest Request Object required for Category Search API + * @param brApiCompletionListener Callback listener + */ + fun categorySearchApi(categorySearchRequest: CategorySearchRequest, brApiCompletionListener: BrApiCompletionListener) { + apiProcessor.processCoreApi(categorySearchRequest.getMap(), brApiCompletionListener) + } + + /** + * Method for calling Content API request + * @param contentSearchRequest Request Object required for Content Search API + * @param brApiCompletionListener Callback listener + */ + fun contentSearchApi(contentSearchRequest: ContentSearchRequest, brApiCompletionListener: BrApiCompletionListener) { + apiProcessor.processCoreApi(contentSearchRequest.getMap(), brApiCompletionListener) + } + + /** + * Method for calling BestSeller API request + * @param bestSellerRequest Request Object required for Content Search API + * @param brApiCompletionListener Callback listener + */ + fun bestSellerApi(bestSellerRequest: BestSellerRequest, brApiCompletionListener: BrApiCompletionListener) { + apiProcessor.processCoreApi(bestSellerRequest.getMap(), brApiCompletionListener) + } + + /** + * Method for calling Suggest API request + * @param autosuggestRequest Request Object required for Content Search API + * @param brApiCompletionListener Callback listener + */ + fun autoSuggestApi(autosuggestRequest: AutosuggestRequest, brApiCompletionListener: BrApiCompletionListener) { + apiProcessor.processSuggestApi(autosuggestRequest.getMap(), brApiCompletionListener) + } + + /* ========= WIDGET API=== */ + + /** + * 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 + * + * @param brApiCompletionListener Callback listener + */ + fun recAndPathwaysWidgetApi(widgetId: String, apiType:WidgetApiType, widgetRequest: WidgetRequest, brApiCompletionListener: BrApiCompletionListener) { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + + recAndPathwaysWidgetApi(widgetId, apiType.value, widgetRequest, brApiCompletionListener) + } + + /** + * 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 + * + * @param brApiCompletionListener Callback listener + */ + fun recAndPathwaysWidgetApi(widgetId: String, + apiType:String, widgetRequest: WidgetRequest, brApiCompletionListener: BrApiCompletionListener) { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + apiProcessor.processRecsAndPathwaysApi(widgetId, apiType, widgetRequest.getMap(), brApiCompletionListener) + } + + /** + * 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 + * + * @param brApiCompletionListener Callback listener + */ + fun itemBasedRecommendationWidgetApi(widgetId: String, widgetRequest: WidgetRequest, brApiCompletionListener: BrApiCompletionListener) { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + recAndPathwaysWidgetApi(widgetId, WidgetApiType.ITEM.value, widgetRequest, brApiCompletionListener) + } + + /** + * 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 + * + * @param brApiCompletionListener Callback listener + */ + fun categoryBasedWidgetApi(widgetId: String, widgetRequest: WidgetRequest, brApiCompletionListener: BrApiCompletionListener) { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + recAndPathwaysWidgetApi(widgetId, WidgetApiType.CATEGORY.value, widgetRequest, brApiCompletionListener) + } + + /** + * 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 + * @param brApiCompletionListener Callback listener + */ + fun keywordBasedWidgetApi(widgetId: String, widgetRequest: WidgetRequest, brApiCompletionListener: BrApiCompletionListener) { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + recAndPathwaysWidgetApi(widgetId, WidgetApiType.KEYWORD.value, widgetRequest, brApiCompletionListener) + } + + + /** + * 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 + * + * @param brApiCompletionListener Callback listener + */ + + fun personalizationBasedWidgetApi(widgetId: String, widgetRequest: WidgetRequest, brApiCompletionListener: BrApiCompletionListener) { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + recAndPathwaysWidgetApi(widgetId, WidgetApiType.PERSONALIZED.value, widgetRequest, brApiCompletionListener) + } + + /** + * 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 + * + * @param brApiCompletionListener Callback listener + */ + fun globalRecommendationWidgetApi(widgetId: String, widgetRequest: WidgetRequest, brApiCompletionListener: BrApiCompletionListener) { + if(widgetId.isEmpty()) { + throw IllegalArgumentException("Widget Id is empty") + } + recAndPathwaysWidgetApi(widgetId, WidgetApiType.GLOBAL.value, widgetRequest, brApiCompletionListener) + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/BrApiRequest.kt b/src/main/java/com/bloomreach/discovery/api/BrApiRequest.kt new file mode 100644 index 0000000..c3d49c1 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/BrApiRequest.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api + +import com.bloomreach.discovery.api.model.Env +import com.bloomreach.discovery.pixel.model.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 + */ +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 +) diff --git a/src/main/java/com/bloomreach/discovery/api/listener/BrApiCompletionListener.kt b/src/main/java/com/bloomreach/discovery/api/listener/BrApiCompletionListener.kt new file mode 100644 index 0000000..b375f63 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/listener/BrApiCompletionListener.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.listener + +import com.bloomreach.discovery.api.model.BrApiError + +/** + * Interface to provide callback with response when API is success and Error when API fails + */ +interface BrApiCompletionListener { + fun onBrApiSuccess(response: Any) + fun onBrApiFailure(error: BrApiError) +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/model/BaseResponse.kt b/src/main/java/com/bloomreach/discovery/api/model/BaseResponse.kt new file mode 100644 index 0000000..67a03d4 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/BaseResponse.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/BrApiError.kt b/src/main/java/com/bloomreach/discovery/api/model/BrApiError.kt new file mode 100644 index 0000000..438f0da --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/BrApiError.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/Env.kt b/src/main/java/com/bloomreach/discovery/api/model/Env.kt new file mode 100644 index 0000000..931cb30 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/Env.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model + +/** + * 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/java/com/bloomreach/discovery/api/model/core/Campaign.kt b/src/main/java/com/bloomreach/discovery/api/model/core/Campaign.kt new file mode 100644 index 0000000..124aacf --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/Campaign.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/CoreResponse.kt b/src/main/java/com/bloomreach/discovery/api/model/core/CoreResponse.kt new file mode 100644 index 0000000..95443b2 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/CoreResponse.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/Doc.kt b/src/main/java/com/bloomreach/discovery/api/model/core/Doc.kt new file mode 100644 index 0000000..7909ce8 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/Doc.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/FacetCounts.kt b/src/main/java/com/bloomreach/discovery/api/model/core/FacetCounts.kt new file mode 100644 index 0000000..b175090 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/FacetCounts.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize + +data class FacetCounts( + + @JsonProperty("facet_fields") + val facetFields: LinkedHashMap>? = null, + + @JsonProperty("facet_queries") + val facetQueries: LinkedHashMap? = null, +// val facetQueries: LinkedHashMap>? = null, + + @JsonProperty("facet_ranges") + val facetRanges: LinkedHashMap>? = null, +) : BaseResponse() \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/model/core/FacetFields.kt b/src/main/java/com/bloomreach/discovery/api/model/core/FacetFields.kt new file mode 100644 index 0000000..a15718b --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/FacetFields.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +class FacetFields : BaseResponse() { + val name: String? = null + + val count: Int = 0 + + @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 +} + diff --git a/src/main/java/com/bloomreach/discovery/api/model/core/FacetRange.kt b/src/main/java/com/bloomreach/discovery/api/model/core/FacetRange.kt new file mode 100644 index 0000000..8d0b33e --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/FacetRange.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.model.BaseResponse + +data class FacetRange( + val start: Any? = null, + val end: Any? = null, + val count: Int? = null, +) : BaseResponse() diff --git a/src/main/java/com/bloomreach/discovery/api/model/core/KeywordRedirect.kt b/src/main/java/com/bloomreach/discovery/api/model/core/KeywordRedirect.kt new file mode 100644 index 0000000..34de90c --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/KeywordRedirect.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/Metadata.kt b/src/main/java/com/bloomreach/discovery/api/model/core/Metadata.kt new file mode 100644 index 0000000..68d670c --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/Metadata.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class Metadata( + @JsonProperty("query") + val query: Query? = null +) : BaseResponse() + diff --git a/src/main/java/com/bloomreach/discovery/api/model/core/Modification.kt b/src/main/java/com/bloomreach/discovery/api/model/core/Modification.kt new file mode 100644 index 0000000..968876d --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/Modification.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/Query.kt b/src/main/java/com/bloomreach/discovery/api/model/core/Query.kt new file mode 100644 index 0000000..351ca0b --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/Query.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/Response.kt b/src/main/java/com/bloomreach/discovery/api/model/core/Response.kt new file mode 100644 index 0000000..e4e681f --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/Response.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/Stats.kt b/src/main/java/com/bloomreach/discovery/api/model/core/Stats.kt new file mode 100644 index 0000000..6f0f9e2 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/Stats.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/StatsField.kt b/src/main/java/com/bloomreach/discovery/api/model/core/StatsField.kt new file mode 100644 index 0000000..908ad44 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/StatsField.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/core/Variant.kt b/src/main/java/com/bloomreach/discovery/api/model/core/Variant.kt new file mode 100644 index 0000000..c628624 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/core/Variant.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.core + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/rp/Category.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Category.kt new file mode 100644 index 0000000..f04ff09 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Category.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +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/java/com/bloomreach/discovery/api/model/rp/Doc.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Doc.kt new file mode 100644 index 0000000..a3e1de1 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Doc.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.model.BaseResponse +import com.bloomreach.discovery.api.model.core.Variant +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() diff --git a/src/main/java/com/bloomreach/discovery/api/model/rp/Facet.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Facet.kt new file mode 100644 index 0000000..710de81 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Facet.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/rp/Field.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Field.kt new file mode 100644 index 0000000..cf23ed7 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Field.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/rp/Metadata.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Metadata.kt new file mode 100644 index 0000000..9335591 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Metadata.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/rp/MetadataResponse.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/MetadataResponse.kt new file mode 100644 index 0000000..aeb73e0 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/MetadataResponse.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/rp/Range.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Range.kt new file mode 100644 index 0000000..a7e8cc0 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Range.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/rp/RecsAndPathwaysResponse.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/RecsAndPathwaysResponse.kt new file mode 100644 index 0000000..ab344ce --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/RecsAndPathwaysResponse.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/rp/Response.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Response.kt new file mode 100644 index 0000000..241ba27 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Response.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.model.BaseResponse +import com.bloomreach.discovery.api.model.core.Doc +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/java/com/bloomreach/discovery/api/model/rp/RpError.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/RpError.kt new file mode 100644 index 0000000..e8942ec --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/RpError.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.fasterxml.jackson.annotation.JsonProperty + +data class RpError( + @JsonProperty("detail") + val detail: String? = null, +) diff --git a/src/main/java/com/bloomreach/discovery/api/model/rp/Value.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Value.kt new file mode 100644 index 0000000..b8996cc --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Value.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/model/rp/Widget.kt b/src/main/java/com/bloomreach/discovery/api/model/rp/Widget.kt new file mode 100644 index 0000000..51c8c9c --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/rp/Widget.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.rp + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +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/java/com/bloomreach/discovery/api/model/suggest/AttributeSuggestion.kt b/src/main/java/com/bloomreach/discovery/api/model/suggest/AttributeSuggestion.kt new file mode 100644 index 0000000..209355d --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/suggest/AttributeSuggestion.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.suggest + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +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/java/com/bloomreach/discovery/api/model/suggest/QueryContext.kt b/src/main/java/com/bloomreach/discovery/api/model/suggest/QueryContext.kt new file mode 100644 index 0000000..e0d2583 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/suggest/QueryContext.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.suggest + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +data class QueryContext( + @JsonProperty("originalQuery") + val originalQuery: String? = null +) : BaseResponse() \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/model/suggest/QuerySuggestion.kt b/src/main/java/com/bloomreach/discovery/api/model/suggest/QuerySuggestion.kt new file mode 100644 index 0000000..0c7ab07 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/suggest/QuerySuggestion.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.suggest + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +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/java/com/bloomreach/discovery/api/model/suggest/SearchSuggestion.kt b/src/main/java/com/bloomreach/discovery/api/model/suggest/SearchSuggestion.kt new file mode 100644 index 0000000..a49242f --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/suggest/SearchSuggestion.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.suggest + +import com.bloomreach.discovery.api.model.BaseResponse +import com.bloomreach.discovery.api.model.core.Variant +import com.fasterxml.jackson.annotation.JsonProperty + +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/java/com/bloomreach/discovery/api/model/suggest/SuggestResponse.kt b/src/main/java/com/bloomreach/discovery/api/model/suggest/SuggestResponse.kt new file mode 100644 index 0000000..bf2f34d --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/suggest/SuggestResponse.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.suggest + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +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/java/com/bloomreach/discovery/api/model/suggest/SuggestionGroup.kt b/src/main/java/com/bloomreach/discovery/api/model/suggest/SuggestionGroup.kt new file mode 100644 index 0000000..b3fc98d --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/model/suggest/SuggestionGroup.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.model.suggest + +import com.bloomreach.discovery.api.model.BaseResponse +import com.fasterxml.jackson.annotation.JsonProperty + +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/java/com/bloomreach/discovery/api/network/ApiProcessor.kt b/src/main/java/com/bloomreach/discovery/api/network/ApiProcessor.kt new file mode 100644 index 0000000..10751ab --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/network/ApiProcessor.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.network + +import android.net.Uri +import com.bloomreach.discovery.api.BrApi +import com.bloomreach.discovery.api.listener.BrApiCompletionListener +import com.bloomreach.discovery.api.model.BrApiError +import com.bloomreach.discovery.api.model.Env +import com.bloomreach.discovery.api.model.core.CoreResponse +import com.bloomreach.discovery.api.model.rp.RecsAndPathwaysResponse +import com.bloomreach.discovery.api.model.suggest.SuggestResponse +import com.bloomreach.discovery.pixel.processpixel.FormatterUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +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 { + + private val CORE_API_PATH = "api/v1/core/" + private val SUGGEST_API_PATH = "api/v2/suggest" + 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 + * @param brApiCompletionListener Interface object to provide success or failure callback + */ + fun processCoreApi( + params: MutableMap, + brApiCompletionListener: BrApiCompletionListener + ) { + var uriBuilder = FormatterUtils.mapToUriBuilderForApi(params) + //add global request parameters + uriBuilder = addGlobalQuery(uriBuilder) + // append base endpoint for API call + addBaseUrlForCoreApi(uriBuilder) + //perform API + CoroutineScope(Dispatchers.IO).launch { + val client = RestClientApi() + + val result = client.doApiCall(URL(uriBuilder.build().toString()), ApiType.CORE) + if (result is CoreResponse) + //invoke success callback + brApiCompletionListener.onBrApiSuccess(result) + else if (result is BrApiError) + //invoke failure callback + brApiCompletionListener.onBrApiFailure(result) + } + } + + /** + * 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 + * @param brApiCompletionListener Interface object to provide success or failure callback + */ + fun processSuggestApi( + params: MutableMap, + brApiCompletionListener: BrApiCompletionListener + ) { + val uriBuilder = FormatterUtils.mapToUriBuilderForApi(params) + //add global request parameters + addGlobalQuery(uriBuilder) + // append base endpoint for API call + addBaseUrlForSuggestApi(uriBuilder) + //perform API + CoroutineScope(Dispatchers.IO).launch { + val client = RestClientApi() + val result = client.doApiCall(URL(uriBuilder.build().toString()), ApiType.SUGGEST) + if (result is SuggestResponse) + //invoke success callback + brApiCompletionListener.onBrApiSuccess(result) + else if (result is BrApiError) + //invoke failure callback + brApiCompletionListener.onBrApiFailure(result) + } + } + + /** + * Method to format Pathways APIs parameters, execute the API 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 widgetType Type of widget + * @param params Map of request parameters to be sent with the request + * @param brApiCompletionListener Interface object to provide success or failure callback + */ + fun processRecsAndPathwaysApi( + widgetId: String, + widgetType: String, + params: MutableMap, + brApiCompletionListener: BrApiCompletionListener + ) { + val uriBuilder = FormatterUtils.mapToUriBuilderForApi(params) + //add global request parameters + addGlobalQuery(uriBuilder) + // append base endpoint for Pixel call + addBaseUrlForPathwaysApi(uriBuilder, widgetType, widgetId) + //perform API + CoroutineScope(Dispatchers.IO).launch { + val client = RestClientApi() + val result = client.doApiCall(URL(uriBuilder.build().toString()), ApiType.PATHWAYS) + if (result is RecsAndPathwaysResponse) + //invoke success callback + brApiCompletionListener.onBrApiSuccess(result) + else if (result is BrApiError) + //invoke failure callback + brApiCompletionListener.onBrApiFailure(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: Uri.Builder): Uri.Builder { + uriBuilder.appendQueryParameter("account_id", BrApi.brApiRequest.accountId) + uriBuilder.appendQueryParameter("auth_key", BrApi.brApiRequest.authKey) + uriBuilder.appendQueryParameter("domain_key", BrApi.brApiRequest.domainKey) + uriBuilder.appendQueryParameter("request_id", FormatterUtils.generateRand()) + uriBuilder.appendQueryParameter( + "_br_uid_2", + FormatterUtils.formatCookieValue( + BrApi.brApiRequest.uuid, + BrApi.brApiRequest.visitorType + ) + ) + uriBuilder.appendQueryParameter("ref_url", "") + if (!BrApi.brApiRequest.userId.isNullOrEmpty()) { + uriBuilder.appendQueryParameter("user_id", BrApi.brApiRequest.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: Uri.Builder) { + uriBuilder.scheme(SCEHEME) + //check for env if stage or prod + when (BrApi.brApiRequest.environment) { + Env.STAGE -> uriBuilder.authority("staging-core.dxpapi.com") + Env.PROD -> uriBuilder.authority("core.dxpapi.com") + } + uriBuilder.appendEncodedPath(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: Uri.Builder) { + uriBuilder.scheme(SCEHEME) + //check for env if stage or prod + when (BrApi.brApiRequest.environment) { + Env.STAGE -> uriBuilder.authority("staging-suggest.dxpapi.com") + Env.PROD -> uriBuilder.authority("suggest.dxpapi.com") + } + uriBuilder.appendEncodedPath(SUGGEST_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: Uri.Builder, widgetType: String, widgetId: String) { + uriBuilder.scheme(SCEHEME) + //check for env if stage or prod + when (BrApi.brApiRequest.environment) { + Env.STAGE -> uriBuilder.authority("pathways-staging.dxpapi.com") + Env.PROD -> uriBuilder.authority("pathways.dxpapi.com") + } + uriBuilder.appendEncodedPath("$WIDGET_API_PATH$widgetType/$widgetId") + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/network/ApiType.kt b/src/main/java/com/bloomreach/discovery/api/network/ApiType.kt new file mode 100644 index 0000000..790a699 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/network/ApiType.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.network + +internal enum class ApiType { + CORE, + SUGGEST, + PATHWAYS +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/network/RestClientApi.kt b/src/main/java/com/bloomreach/discovery/api/network/RestClientApi.kt new file mode 100644 index 0000000..b724d41 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/network/RestClientApi.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.network + +import android.util.Log +import com.bloomreach.discovery.BuildConfig +import com.bloomreach.discovery.api.BrApi +import com.bloomreach.discovery.api.model.BrApiError +import com.bloomreach.discovery.api.model.core.CoreResponse +import com.bloomreach.discovery.api.model.rp.RecsAndPathwaysResponse +import com.bloomreach.discovery.api.model.rp.RpError +import com.bloomreach.discovery.api.model.suggest.SuggestResponse +import com.fasterxml.jackson.databind.ObjectMapper +import java.io.BufferedReader +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL + +/** + * Class to perform API call for API module + */ +internal class RestClientApi { + + /** + * 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. + * @return Any response object CoreResponse if API call success else return BrApiError object + */ + fun doApiCall(url: URL, type: ApiType): Any? { + val inputStream: InputStream + try { + Log.i("$type API CALL:", url.toString()) + + // Create HttpURLConnection + val conn: HttpURLConnection = url.openConnection() as HttpURLConnection + // API request set to GET + conn.requestMethod = "GET"; + // set none cache + conn.setRequestProperty("Cache-Control", "no-cache") + conn.setRequestProperty( + "User-Agent", + "Bloomreach/${BuildConfig.SDK_VERSION} " + System.getProperty("http.agent") + ) + + if (type == ApiType.PATHWAYS) { + //v2 API requires passing the auth-key as a request + conn.setRequestProperty("auth_key", BrApi.brApiRequest.authKey) + } + + conn.defaultUseCaches = false; + conn.useCaches = false; + // Launch GET request + conn.connect() + + val responseCode = conn.responseCode + Log.i("$type API CALL:", "responseCode: $responseCode") + if (responseCode in 200..299) { // success + // Receive response as inputStream + inputStream = conn.inputStream + + if (inputStream != null) { + // Convert input stream to string + var result = inputStream.bufferedReader().use(BufferedReader::readText) + inputStream.close() + val responseMapper = ObjectMapper() + return when (type) { + ApiType.CORE -> responseMapper.readValue(result, CoreResponse::class.java) + ApiType.SUGGEST -> responseMapper.readValue( + result, + SuggestResponse::class.java + ) + else -> responseMapper.readValue(result, RecsAndPathwaysResponse::class.java) + } + } else { + inputStream.close() + return BrApiError("Something went wrong", responseCode) + } + } else { + if (conn.errorStream != null) { + val result = conn.errorStream.bufferedReader().use(BufferedReader::readText) + conn.errorStream.close() + //covert error result to BrApiError object + print("error: $result") + return if (type == ApiType.PATHWAYS) { + val responseMapper = ObjectMapper() + val rpError = responseMapper.readValue(result, RpError::class.java) + BrApiError(rpError.detail ?: "Something went wrong", responseCode) + } else { + val responseMapper = ObjectMapper() + responseMapper.readValue(result, BrApiError::class.java) + } + } + return BrApiError("Something went wrong", responseCode) + } + } catch (err: Exception) { + Log.e("$type API CALL:", "Error: ${err.localizedMessage}") + return BrApiError("Something went wrong", 0) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/request/AutosuggestRequest.kt b/src/main/java/com/bloomreach/discovery/api/request/AutosuggestRequest.kt new file mode 100644 index 0000000..748dba4 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/AutosuggestRequest.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.request + +import com.bloomreach.discovery.api.ApiConstants +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 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/java/com/bloomreach/discovery/api/request/BestSellerRequest.kt b/src/main/java/com/bloomreach/discovery/api/request/BestSellerRequest.kt new file mode 100644 index 0000000..628e1cc --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/BestSellerRequest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.request + +import com.bloomreach.discovery.api.ApiConstants + +/** + * 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/java/com/bloomreach/discovery/api/request/CategorySearchRequest.kt b/src/main/java/com/bloomreach/discovery/api/request/CategorySearchRequest.kt new file mode 100644 index 0000000..0c17408 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/CategorySearchRequest.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.request + +import com.bloomreach.discovery.api.ApiConstants + +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) + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/request/ContentSearchRequest.kt b/src/main/java/com/bloomreach/discovery/api/request/ContentSearchRequest.kt new file mode 100644 index 0000000..0bbfada --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/ContentSearchRequest.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.request + +import com.bloomreach.discovery.api.ApiConstants + +/** + * 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/java/com/bloomreach/discovery/api/request/Operator.kt b/src/main/java/com/bloomreach/discovery/api/request/Operator.kt new file mode 100644 index 0000000..2f6aefa --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/Operator.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/request/ProductSearchRequest.kt b/src/main/java/com/bloomreach/discovery/api/request/ProductSearchRequest.kt new file mode 100644 index 0000000..c6dcdef --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/ProductSearchRequest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.request + +import com.bloomreach.discovery.api.ApiConstants.REQUEST_TYPE +import com.bloomreach.discovery.api.ApiConstants.REQUEST_TYPE_SEARCH +import com.bloomreach.discovery.api.ApiConstants.SEARCH_TYPE +import com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/request/RequestMap.kt b/src/main/java/com/bloomreach/discovery/api/request/RequestMap.kt new file mode 100644 index 0000000..d78d188 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/RequestMap.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.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 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/java/com/bloomreach/discovery/api/request/SearchRequest.kt b/src/main/java/com/bloomreach/discovery/api/request/SearchRequest.kt new file mode 100644 index 0000000..03ddf4a --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/SearchRequest.kt @@ -0,0 +1,494 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.request + +import com.bloomreach.discovery.api.ApiConstants +import com.bloomreach.discovery.api.ApiConstants.DEFAULT_ROWS +import com.bloomreach.discovery.api.ApiConstants.DEFAULT_START +import com.bloomreach.discovery.api.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 Array 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) + } + + /** + * 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 values The list of stats.field values + * + * @return A reference to the current Request object + */ + 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) + } + + /** + * 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 values The array of stats.field values + * + * @return A reference to the current Request object + */ + 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/java/com/bloomreach/discovery/api/request/Sort.kt b/src/main/java/com/bloomreach/discovery/api/request/Sort.kt new file mode 100644 index 0000000..cc95dc5 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/Sort.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/request/SortOrder.kt b/src/main/java/com/bloomreach/discovery/api/request/SortOrder.kt new file mode 100644 index 0000000..a7baf68 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/SortOrder.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.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/java/com/bloomreach/discovery/api/request/WidgetApiType.kt b/src/main/java/com/bloomreach/discovery/api/request/WidgetApiType.kt new file mode 100644 index 0000000..3d56176 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/WidgetApiType.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.request + +/** + * Widget TYPE ENUM to specify which type on widget API needs to be called. + * This gets added as Path parameter to he request + */ +enum class WidgetApiType(val value: String) { + ITEM("item"), + CATEGORY("category"), + KEYWORD("keyword"), + PERSONALIZED("personalized"), + GLOBAL("global") +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/api/request/WidgetRequest.kt b/src/main/java/com/bloomreach/discovery/api/request/WidgetRequest.kt new file mode 100644 index 0000000..dbb4885 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/api/request/WidgetRequest.kt @@ -0,0 +1,407 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.api.request + +import com.bloomreach.discovery.api.ApiConstants +import com.bloomreach.discovery.api.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) + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/PixelTracker.kt b/src/main/java/com/bloomreach/discovery/pixel/PixelTracker.kt new file mode 100644 index 0000000..777aa12 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/PixelTracker.kt @@ -0,0 +1,567 @@ +/* + * Copyright 2022 Bloomreach + */ +package com.bloomreach.discovery.pixel + +import android.util.Log +import com.bloomreach.discovery.pixel.processpixel.PixelProcessor +import com.bloomreach.discovery.pixel.model.* + +/** + * PixelTracker Singleton class holds all types of Page view and Event Pixels methods + */ +object PixelTracker { + + private val TAG: String = PixelTracker.javaClass.simpleName + lateinit var brPixel: BrPixel + private val pixelProcessor: PixelProcessor = PixelProcessor() + + /** + * Initialise Pixel tracker with BrPixel object + * @param brPixel BrPixel object defined for initialisation + */ + fun init(brPixel: BrPixel) { + this.brPixel = brPixel + } + + /** + * Method for sending the Page View Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + */ + fun pageViewPixel(ref: String, title: String, viewId: String? = null) { + if (this::brPixel.isInitialized) { + // create pixel object based on input + val pixelObject = PixelObject( + type = PixelType.PAGE_VIEW, + pType = PageType.HOME_PAGE, + ref = ref, + title = title + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Product Page View Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param prodId This is the unique ID which describes a product or product collection. + * @param prodName The name of the product being viewed. + * @param sku Unique sku ID that represents the selected variant of this product. If your site does not have SKUs, leave this blank. + */ + fun productPageViewPixel( + ref: String, + title: String, + prodId: String, + prodName: String, + sku: String? = null + ) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.PAGE_VIEW, pType = PageType.PRODUCT_PAGE, + ref = ref, title = title, prodId = prodId, prodName = prodName, prodSku = sku + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Content Page View Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param catalogs List of CatalogItem that are shown in the page. In case the page has multiple + * tabs, only the catalogs of the selected (and visualized) tabs should be included. + * If multiple catalogs are shown in the active page (or tab) all of them should be included. + * @param itemId Unique ID of the item being shown in the page. This identifier should match + * the item_id as specified in the content feed. + * @param itemName Name or the title of the content page. + */ + fun contentPageViewPixel( + ref: String, + title: String, + catalogs: List, + itemId: String, + itemName: String + ) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.PAGE_VIEW, + pType = PageType.CONTENT_PAGE, + ref = ref, + title = title, + catalogs = catalogs, + itemId = itemId, + itemName = itemName + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Content Search Page View Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param catalogs List of CatalogItem that are shown in the page. + * @param searchTerm The value of the search query describing the page. + */ + fun contentSearchPageViewPixel( + ref: String, + title: String, + catalogs: List, + searchTerm: String + ) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.PAGE_VIEW, + pType = PageType.SEARCH_PAGE, + ref = ref, + title = title, + catalogs = catalogs, + searchTerm = searchTerm + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Category Search Page View Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param categoryId Unique category ID as referred to in the database/catalog. Bloomreach requires the cat_id field to be unique across your site. + * @param category The bread crumb of the page. Value needs to match the crumb value in your feed. eg: "Home|Clothing|Outerwear" + */ + fun categoryPageViewPixel( + ref: String, + title: String, + categoryId: String, + category: String + ) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.PAGE_VIEW, + pType = PageType.CATEGORY_PAGE, + ref = ref, + title = title, + cat = category, + catId = categoryId + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Search Result Page View Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param searchTerm The value of the search query describing the page. + */ + fun searchResultPageViewPixel(ref: String, title: String, searchTerm: String) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.PAGE_VIEW, + pType = PageType.SEARCH_PAGE, + ref = ref, + title = title, + searchTerm = searchTerm + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Conversion Page View Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param isConversion Set to true to indicate this is a Conversion or Thank you page + * @param basketValue The total price of the checkout basket including tax, discounts, shipping and/or discounts in the account currency. + * @param orderId The order ID associated with the order placed + * @param basket List of the PixelBasketItem objects (Products purchased). + */ + fun conversionPageView( + ref: String, + title: String, + isConversion: Boolean, + basketValue: String, + orderId: String, + basket: List + ) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.PAGE_VIEW, + pType = PageType.CONVERSION, + ref = ref, + title = title, + isConversion = if (isConversion) 1 else 0, + basketValue = basketValue, + orderId = orderId, + basket = basket + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method to send any type of Page View Pixel with Custom Parameters + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param pType Maps your site's page type classifications to the values Bloomreach expects for our page type classifications. + * @param params Map for custom keys and its associated values + */ + fun customPageViewPixel( + ref: String, + title: String, + pType: PageType, + params: MutableMap + ) { + if (this::brPixel.isInitialized) { + // directly add map to the PixelQueue + params["ref"] = ref + params["title"] = title + params["type"] = PixelType.PAGE_VIEW.type + params["ptype"] = pType.pType + pixelProcessor.processPixel(params) + + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + //==================== EVENT PIXELS ======================= + + /** + * Method for sending the Add To Cart Event Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param prodId This is the unique ID which describes a product or product collection. + * @param prodName The name of the product being viewed. + * @param sku Unique sku ID that represents the selected variant of this product. If your site does not have SKUs, leave this blank. + * @param prodCollectionId (Optional) When a product is added to cart from a Product Collection page, set prod_collection_id as the id of the collection. + * No need to set prod_collection_id param in the ATC event pixel when a product is added to cart from its own page, independent of any Product Collection it is part of. + */ + fun addToCartEventPixel( + ref: String, + title: String, + prodId: String, + prodName: String, + sku: String, + prodCollectionId: String? = null + ) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.EVENT, + pType = PageType.PRODUCT_PAGE, + group = GroupType.CART, + eType = "click-add", + ref = ref, + title = title, + prodId = prodId, + prodName = prodName, + prodSku = sku, + prodCollectionId = prodCollectionId + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Search Event Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param prodId This is the unique ID which describes a product or product collection. + * @param prodName The name of the product being viewed. + * @param sku Unique sku ID that represents the selected variant of this product. If your site does not have SKUs, leave this blank. + * @param searchTerm The value of the search query describing the page. + * @param catalogs List of CatalogItem that are shown in the page. + */ + fun searchEventPixel( + ref: String, + title: String, + prodId: String, + prodName: String, + sku: String, + searchTerm: String, + catalogs: List? = null + ) { + if (this::brPixel.isInitialized) { + + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.EVENT, + pType = PageType.PRODUCT_PAGE, + group = GroupType.SUGGEST, + eType = "submit", + ref = ref, + title = title, + prodId = prodId, + prodName = prodName, + prodSku = sku, + searchTerm = searchTerm, + catalogs = catalogs + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Suggestion Event Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param prodId This is the unique ID which describes a product or product collection. + * @param prodName The name of the product being viewed. + * @param sku Unique sku ID that represents the selected variant of this product. If your site does not have SKUs, leave this blank. + * @param typedTerm The display query (the one or more letters) that the user has actually typed. + This is NOT the suggested word or phrase. + * @param searchTerm User's typed search query submitted to search box + * @param catalogs List of CatalogItem that are shown in the page. + */ + fun suggestionEventPixel( + ref: String, + title: String, + prodId: String, + prodName: String, + sku: String, + typedTerm: String, + searchTerm: String, + catalogs: List? = null + ) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.EVENT, + pType = PageType.PRODUCT_PAGE, + group = GroupType.SUGGEST, + eType = "click", + ref = ref, + title = title, + prodId = prodId, + prodName = prodName, + prodSku = sku, + searchTerm = searchTerm, + typedTerm = typedTerm, + catalogs = catalogs + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Quick Event Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param prodId This is the unique ID which describes a product or product collection. + * @param prodName The name of the product being viewed. + * @param sku Unique sku ID that represents the selected variant of this product. If your site does not have SKUs, leave this blank. + */ + fun quickViewEventPixel( + ref: String, + title: String, + prodId: String, + prodName: String, + sku: String + ) { + if (this::brPixel.isInitialized) { + // create pixel object based ob input + val pixelObject = PixelObject( + type = PixelType.EVENT, + pType = PageType.PRODUCT_PAGE, + group = GroupType.PRODUCT, + eType = "quickview", + ref = ref, + title = title, + prodId = prodId, + prodName = prodName, + prodSku = sku + ) + + // send pixel for further processing + pixelProcessor.processPixel(pixelObject) + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Custom Event Pixel + * @param ref Synthetic URL from referrer screen + * @param title Screen name of the app view. + * @param eType Event type + * @param pType Maps your site's page type classifications to the values Bloomreach expects for our page type classifications. + * @param group Specifies the event grouping + * @param params Map for custom keys and its associated values + */ + fun customEventPixel( + ref: String, + title: String, + eType: String, + pType: PageType, + group: GroupType, + params: MutableMap + ) { + if (this::brPixel.isInitialized) { + // directly add map to the PixelQueue + params["ref"] = ref + params["title"] = title + params["type"] = PixelType.EVENT.type + params["etype"] = eType + params["ptype"] = pType.pType + params["group"] = group.group + pixelProcessor.processPixel(params) + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + //==================== WIDGET PIXELS ======================= + + /** + * Method for sending the Widget View Pixel + * @param widgetDataWrId The unique ID of the response. This value has to be populated from the API response metadata.widget.rid. + * @param widgetViewDataWq The query string used by the customer which returns a widget suggestion. This is optional for non-query widgets. + * @param widgetViewDataWid The widget ID. This is a unique, 6 character alphanumeric value. This value has to be populated from the API response metadata.widget.id. + * @param widgetViewDataWty The type of recommendation widget. This value has to be populated from the API response.This value has to be populated from the API response metadata.widget.type. + */ + fun widgetView( + widgetDataWrId: String, + widgetViewDataWq: String, + widgetViewDataWid: String, + widgetViewDataWty: String + ) { + if (this::brPixel.isInitialized) { + val params = mutableMapOf() + params["type"] = PixelType.EVENT.type + params["group"] = GroupType.WIDGET.group + params["etype"] = "widget-view" + params["wrid"] = widgetDataWrId + params["wq"] = widgetViewDataWq + params["wid"] = widgetViewDataWid + params["wty"] = widgetViewDataWty + pixelProcessor.processPixel(params) + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Widget Click Pixel + * @param widgetDataWrId The unique ID of the response. + * @param widgetViewDataWq The query string used by the customer which returns a widget suggestion. This is optional for non-query widgets. + * @param widgetViewDataWid The widget ID. This is a unique, 6 character alphanumeric value. This value has to be populated from the API response metadata.widget.id. + * @param widgetViewDataWty The type of recommendation widget. This value has to be populated from the API response.This value has to be populated from the API response metadata.widget.type. + * @param widgetDataItemId The unique ID (PID) of the clicked product. The PID is used from the API call. + */ + fun widgetClick( + widgetDataWrId: String, + widgetViewDataWq: String, + widgetViewDataWid: String, + widgetViewDataWty: String, + widgetDataItemId: String + ) { + if (this::brPixel.isInitialized) { + val params = mutableMapOf() + params["type"] = PixelType.EVENT.type + params["group"] = GroupType.WIDGET.group + params["etype"] = "widget-click" + params["ptype"] = PageType.OTHER_PAGE.pType + + params["item_id"] = widgetDataItemId + params["wrid"] = widgetDataWrId + params["wq"] = widgetViewDataWq + params["wid"] = widgetViewDataWid + params["wty"] = widgetViewDataWty + pixelProcessor.processPixel(params) + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } + + /** + * Method for sending the Widget Add to cart + * @param widgetDataWrId The unique ID of the response. This value has to be populated from the API response metadata.widget.rid. + * @param widgetViewDataWq The query string used by the customer which returns a widget suggestion. This is optional for non-query widgets. + * @param widgetViewDataWid The widget ID. This is a unique, 6 character alphanumeric value. This value has to be populated from the API response metadata.widget.id. + * @param widgetViewDataWty The type of recommendation widget. This value has to be populated from the API response.This value has to be populated from the API response metadata.widget.type. + * @param widgetDataItemId The unique ID (PID) of the clicked product. The PID is used from the API call. + * @param widgetAtcDataSku Unique SKU id that represents the selected variant of this product (e.g. size M, color blue of a t-shirt). If your site does not have SKUs, leave this blank. + */ + fun widgetAddToCart( + widgetDataWrId: String, + widgetViewDataWid: String, + widgetViewDataWq: String, + widgetViewDataWty: String, + widgetDataItemId: String, + widgetAtcDataSku: String? = null + ) { + if (this::brPixel.isInitialized) { + val params = mutableMapOf() + params["type"] = PixelType.EVENT.type + params["group"] = GroupType.CART.group + params["etype"] = "widget-add" + params["item_id"] = widgetDataItemId + params["wrid"] = widgetDataWrId + params["wid"] = widgetViewDataWid + params["wq"] = widgetViewDataWq + params["wty"] = widgetViewDataWty + if(!widgetAtcDataSku.isNullOrEmpty()) { + params["sku"] = widgetAtcDataSku + } + pixelProcessor.processPixel(params) + } else { + Log.e(TAG, "Pixel Tracker not initialised") + } + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/BrPixel.kt b/src/main/java/com/bloomreach/discovery/pixel/model/BrPixel.kt new file mode 100644 index 0000000..0ffc361 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/BrPixel.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Bloomreach + */ +package com.bloomreach.discovery.pixel.model + +/** + * Class containing initialising parameters for the Pixel SDK. + * + * @property accountId Account Id provided by Bloomreach + * @property uuid Android Advertising ID + * @property visitorType ENUM type for New User or returning user + * @property baseUrl Base Url for the merchant provided bt Bloomreach + * @property domainKey The Bloomreach-provided ID of the domain receiving the request.(Optional) + * @property userId This parameter is only required if you track users via a universal customer ID. + * @property testData Do not declare test_data in the pixel for your live site. + * @property currency Currency for the app + * @property pixelUrlByRegion Url for Pixel server based on region. Default to NA region + * @property customerTier Tier that the user belongs to. eg: Premium, Gold + * @property customerCountry Country that the user belongs to or is accessing the site from. + * @property customerGeo Geography or Region that the user belongs to. + * @property customerProfile Profile of the user. + * @property viewId View Id + * @property debugMode Debug mode of main application to see Pixel Validator logs. Pass value as BuildConfig.DEBUG + */ + +data class BrPixel( + val accountId: String, + val uuid: String, + val visitorType: VisitorType, + val baseUrl: String, + var domainKey: String? = null, + var userId: String? = null, + var testData: Boolean = false, + var currency: String? = null, + var pixelUrlByRegion: String = PixelRegion.NA.url, + //segments + var customerTier: String? = null, + var customerCountry: String? = null, + var customerGeo: String? = null, + var customerProfile: String? = null, + var viewId: String? = null, + var debugMode: Boolean = false +) { + init { + require(accountId.isNotEmpty()) { "Bloomreach Account Id is required" } + require(uuid.isNotEmpty()) { "UUID is required" } + // require(visitor.isNotEmpty()) { "visitor is required" } + require(baseUrl.isNotEmpty()) { "baseUrl is required" } + } +} diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/CatalogItem.kt b/src/main/java/com/bloomreach/discovery/pixel/model/CatalogItem.kt new file mode 100644 index 0000000..20317c4 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/CatalogItem.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model + +/** + * Model class for Catalog Item + * @property name Catalog Name + * @property viewIds Views show selective content to selective users, such as content-based on regions or stores. This can be Single or Multiple. + */ +data class CatalogItem( + val name: String, + val viewIds: List? = null +) diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/GroupType.kt b/src/main/java/com/bloomreach/discovery/pixel/model/GroupType.kt new file mode 100644 index 0000000..5e3736b --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/GroupType.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model + +/** + * GroupType ENUM for events pixels group + */ +enum class GroupType(val group: String) { + CART("cart"), + SUGGEST("suggest"), + PRODUCT("product"), + WIDGET("widget") +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/PageType.kt b/src/main/java/com/bloomreach/discovery/pixel/model/PageType.kt new file mode 100644 index 0000000..01b09e5 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/PageType.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Bloomreach + */ +package com.bloomreach.discovery.pixel.model + +/** + * PageType ENUM to provide page name (pType parameter) for Pixels + */ +enum class PageType(val pType: String) { + // Any home page or landing page that is considered a home page needs to be classified as + HOME_PAGE("homepage"), + + // Any product, product bundle, product collection or sku set pages need to be classified as + PRODUCT_PAGE("product"), + + // Any category listing pages, category product listing pages or pages that you consider a category page need to be classified as + CATEGORY_PAGE("category"), + + // Any search listing or search results pages need to be classified as + SEARCH_PAGE("search"), + + // Any content pages need to be classified as + CONTENT_PAGE("content"), + + // Bloomreach Thematic pages need to be classified as + THEMATIC_PAGE("thematic"), + + // Any Conversion/ Thank You pages as well as any page types + CONVERSION("conversion"), + + //Any page types that are not one of the above need to be classified as + OTHER_PAGE("other") +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/PixelBasketItem.kt b/src/main/java/com/bloomreach/discovery/pixel/model/PixelBasketItem.kt new file mode 100644 index 0000000..c208213 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/PixelBasketItem.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model + +/** + * Class containing product details for the purchase. + * @property prodId Product Id of the purchased product + * @property name Name of the purchased product + * @property sku SKU number of the purchased product + * @property quantity Purchased quantity of the product + * @property price Price of the product + */ +data class PixelBasketItem( + val prodId: String, + val name: String, + val sku: String?, + val quantity: String, + val price: String +) diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/PixelObject.kt b/src/main/java/com/bloomreach/discovery/pixel/model/PixelObject.kt new file mode 100644 index 0000000..a10fe52 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/PixelObject.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model + +/** + * Model class for PixelObject. Pixel Object will hold all the fields provided by merchant app + * and will be used for further process and converting it to required Query Parameter + */ +internal data class PixelObject( + val type: PixelType, //ENUM + val pType: PageType, //ENUM + val title: String, + val ref: String, + + val eType: String? = null, + val prodId: String? = null, + val prodName: String? = null, + val prodSku: String? = null, + val prodCollectionId: String? = null, + + val catalogs: List? = null, //Array of catalogs + val itemId: String? = null, + val itemName: String? = null, + + val isConversion: Int? = null, + val basketValue: String? = null, + val orderId: String? = null, + val basket: List? = null, + + val group: GroupType? = null, + val searchTerm: String? = null, + val typedTerm: String? = null, + val catId: String? = null, + val cat: String? = null + +) diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/PixelRegion.kt b/src/main/java/com/bloomreach/discovery/pixel/model/PixelRegion.kt new file mode 100644 index 0000000..a2e3e22 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/PixelRegion.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model + +/** + * PixelRegion ENUM to provide pixel end point based on region + */ +enum class PixelRegion(val url: String) { + NA("p.brsrvr.com"), + EU("p-eu.brsrvr.com"), + TEST("p-test.brsrvr.com") +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/PixelType.kt b/src/main/java/com/bloomreach/discovery/pixel/model/PixelType.kt new file mode 100644 index 0000000..a2ab1b6 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/PixelType.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model + +/** + * PixelType ENUM to provide tne type of Pixel to be sent + */ +enum class PixelType(val type: String) { + // for page view pixels + PAGE_VIEW("pageview"), + + // for event pixels + EVENT("event") +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/VisitorType.kt b/src/main/java/com/bloomreach/discovery/pixel/model/VisitorType.kt new file mode 100644 index 0000000..3fcc8c3 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/VisitorType.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model + +/** + * 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/java/com/bloomreach/discovery/pixel/model/validator/PixelValidatorBody.kt b/src/main/java/com/bloomreach/discovery/pixel/model/validator/PixelValidatorBody.kt new file mode 100644 index 0000000..da9c202 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/validator/PixelValidatorBody.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model.validator + +/** + * Request Body for posting to Pixel Validator call + */ +internal data class PixelValidatorBody( + val source: String, + val protocol: String, + val host: String, + val port: String, + val query: String, + val params: MutableMap, + val file: String, + val hash: String, + val path: String, + val relative: String, + val segments: List +) diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/validator/response/Param.kt b/src/main/java/com/bloomreach/discovery/pixel/model/validator/response/Param.kt new file mode 100644 index 0000000..da304c8 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/validator/response/Param.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model.validator.response + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Param Response object from Pixel Validator API response + */ +data class Param( + @JsonProperty("description") + val description: String?, + @JsonProperty("name") + val name: String, + @JsonProperty("value") + val value: String +) \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/model/validator/response/PixelValidatorResponse.kt b/src/main/java/com/bloomreach/discovery/pixel/model/validator/response/PixelValidatorResponse.kt new file mode 100644 index 0000000..4ea4b47 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/model/validator/response/PixelValidatorResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.model.validator.response + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Response object from Pixel Validator API response + */ +data class PixelValidatorResponse( + @JsonProperty("displayName") + val displayName: String, + @JsonProperty("errors") + val errors: List, + @JsonProperty("merchant") + val merchant: String, + @JsonProperty("params") + val params: List?, + @JsonProperty("success") + val success: List, + @JsonProperty("url") + val url: String, + @JsonProperty("warns") + val warns: List +) \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/network/RestClient.kt b/src/main/java/com/bloomreach/discovery/pixel/network/RestClient.kt new file mode 100644 index 0000000..32a6953 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/network/RestClient.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.network + +import android.net.Uri +import android.util.Log +import com.bloomreach.discovery.BuildConfig +import com.bloomreach.discovery.pixel.PixelTracker +import com.bloomreach.discovery.pixel.model.validator.PixelValidatorBody +import com.bloomreach.discovery.pixel.model.validator.response.PixelValidatorResponse +import com.fasterxml.jackson.databind.ObjectMapper +import java.io.BufferedReader +import java.io.InputStream +import java.io.OutputStream +import java.io.OutputStreamWriter +import java.net.URL +import javax.net.ssl.HttpsURLConnection + +/** + * RestClient class to perform API call in the SDK + */ +internal class RestClient { + + private val pixelValidatorUrl = "https://tools.bloomreach.com/pixel-validator/validatePixel" + + /** + * Method to call Pixel Validator API + * @param postBody required post body to be sent to Pixel Validator API + * + * @return PixelValidatorResponse response object + */ + fun validatePixel(postBody: PixelValidatorBody): PixelValidatorResponse? { + val inputStream: InputStream + var result: String? = null + + try { + // Create URL + val url = URL(pixelValidatorUrl) + +// Log.i("", pixelValidatorUrl) + // Create HttpURLConnection + val conn: HttpsURLConnection = url.openConnection() as HttpsURLConnection + conn.doOutput = true + // set request method POST + conn.requestMethod = "POST" + + // set required headers + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Accept", "application/json"); + + // Jackson Convertor ObjectMapper class for converting Model to required String to be sent as POST body + val mapper = ObjectMapper() + + // OutputStream to send post request body + val os: OutputStream = conn.outputStream + val osw = OutputStreamWriter(os, "UTF-8") + osw.write(mapper.writeValueAsString(postBody)) + + osw.flush() + osw.close() + os.close() //close the OutputStream + + // Launch POST request + conn.connect() + if (conn.responseCode in 200..299) { + // API Success + // Receive response as inputStream + inputStream = conn.inputStream + + if (inputStream != null) { + // Convert input stream to string + result = inputStream.bufferedReader().use(BufferedReader::readText) + + //Jackson Convertor ObjectMapper convert respnse json to PixelValidatorResponse Object + inputStream.close() + val responseMapper = ObjectMapper() + return responseMapper.readValue(result, PixelValidatorResponse::class.java) + } else { + inputStream.close() + Log.e("PixelValidator", "PixelValidator failure") + } + } else { + // API failure + Log.e("PixelValidator", "PixelValidator failure with response code ${conn.responseCode}") + } + } catch (err: Exception) { + Log.e("PixelValidator", "Error when executing get request: ${err.localizedMessage}") + } + return null + } + + /** + * Method to call Submit Pixel API for all types of Pixel + * @param uriBuilder Uri.Builder with query parametes to be sent + * + * @return String to know success or error + */ + fun submitPixel(uriBuilder: Uri.Builder): String? { + // Pass each Pixel through through retry Retry Strategy + val retryStrategy = RetryStrategy() + //check if retry is needed + while (retryStrategy.shouldRetry()) { + val inputStream: InputStream + var result: String? = null + try { + // append base endpoint for Pixel call + uriBuilder.scheme("https") + uriBuilder.authority(PixelTracker.brPixel.pixelUrlByRegion) + uriBuilder.appendPath("pix.gif") + // Create URL + val url = URL(uriBuilder.build().toString()) + + Log.i("Submit Pixel ", uriBuilder.toString()) + + // Create HttpURLConnection + val conn: HttpsURLConnection = url.openConnection() as HttpsURLConnection + // conn.doOutput = true + // API request set to GET + conn.requestMethod = "GET"; + // set none cache + conn.setRequestProperty("Cache-Control", "no-cache") + + // set user agent property with required SDK version + + conn.setRequestProperty( + "User-Agent", + "Bloomreach/${BuildConfig.SDK_VERSION} " + System.getProperty("http.agent") + ) + + conn.defaultUseCaches = false; + conn.useCaches = false; + // Launch GET request + conn.connect() + + val responseCode = conn.responseCode + Log.i("SubmitPixel", "responseCode: $responseCode") + if (responseCode in 200..299) { // success + // Receive response as inputStream + inputStream = conn.inputStream + + if (inputStream != null) { + // Convert input stream to string + result = inputStream.bufferedReader().use(BufferedReader::readText) + + inputStream.close() + return result + } else { + result = "error: inputStream is null" + inputStream.close() + return result + } + } else if (responseCode in 400..499) { + // skip the pixel and don't retry + return null + } else { + // if error inform retryStrategy class to check if to continue with retry + retryStrategy.errorOccured() + } + + } catch (err: Exception) { + Log.e("SubmitPixel", "Error: ${err.localizedMessage}") + retryStrategy.errorOccured() + } + } + return null + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/network/RetryStrategy.kt b/src/main/java/com/bloomreach/discovery/pixel/network/RetryStrategy.kt new file mode 100644 index 0000000..5f90ebb --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/network/RetryStrategy.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.network + +/** + * RetryStrategy - Retry mechanism for Pixel call with network failure scenerios + */ +class RetryStrategy { + + private val DEFAULT_RETRIES = 3 + private val DEFAULT_WAIT_TIME_IN_MILLI: Long = 1000 + + private var numberOfRetries = DEFAULT_RETRIES + private var numberOfTriesLeft = 3 + private var timeToWait: Long = DEFAULT_WAIT_TIME_IN_MILLI + + /** + * @return true if there are tries left + */ + fun shouldRetry(): Boolean { + return numberOfTriesLeft > 0 + } + + @Throws(Exception::class) + fun errorOccured() { + if (!shouldRetry()) { + throw Exception( + "Retry Failed: Total " + numberOfRetries + + " attempts made at interval " + getTimeToWait() + + "ms" + ) + } + numberOfTriesLeft-- + waitUntilNextTry() + } + + private fun getTimeToWait(): Long { + return timeToWait + } + + private fun waitUntilNextTry() { + try { + Thread.sleep(getTimeToWait()) + } catch (ignored: InterruptedException) { + + } + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/processpixel/FormatterUtils.kt b/src/main/java/com/bloomreach/discovery/pixel/processpixel/FormatterUtils.kt new file mode 100644 index 0000000..98a56ef --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/processpixel/FormatterUtils.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2022 Bloomreach + */ +package com.bloomreach.discovery.pixel.processpixel + +import android.net.Uri +import android.util.Log +import com.bloomreach.discovery.pixel.model.CatalogItem +import com.bloomreach.discovery.pixel.model.PixelBasketItem +import com.bloomreach.discovery.pixel.model.VisitorType +import java.net.URLDecoder +import java.net.URLEncoder +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=app:ts=0:hc=${hitcount.hitCount}" + } + + /** + * Method to format url value + * @param baseurl Base Url for the merchant provided bt Bloomreach + * @param pType Page classification type + * @param title Title of the screen + * @return url - String value in 'http://merchantname.app/ptype/title' format + */ + fun formatUrl(baseurl: String, pType: String, title: String): String { + // convert in format http://merchantname.app/ptype/title + return "$baseurl$pType/$title" + } + + /** + * Method to format catalog value + * The catalog name is encoded by prefixing "cat" + "the index of the catalog starting from 0" + "=" + "the catalog name" + * @param catalogs Array of the CatalogItem objects + * @return cataLogs - String value in required format + */ + fun formatCatalog(catalogs: List?): String { + + if (catalogs.isNullOrEmpty()) { + Log.i(FormatterUtils.javaClass.simpleName, "Catalogs is Null or Empty") + return "" + } + return buildString { + for ((catalogIndex, catalog) in catalogs.withIndex()) { + // Each catalog in catalogs is separated by an "!" + if (catalogIndex != 0) this.append("!") + // add "cat" + "the index of the catalog starting from 0" + this.append("cat$catalogIndex=${catalog.name}") + + if (!catalog.viewIds.isNullOrEmpty()) { + for ((viewIndex, viewId) in catalog.viewIds.withIndex()) { + // Catalog name and view_id are separated by a ":" + if (viewIndex == 0) this.append(":") + // Multiple view_ids are separated by a "." + else this.append(".") + // The view_id is encoded by prefixing "v" + "the index of the view_id starting from 0" + "=" + "the view_id" + this.append("v$viewIndex=$viewId") + } + } + } + } + } + + /** + * Method to format basket value + * Each product in the cart will be separated by !. Each product's details will be separated by '. + * @param basketItems Array of the PixelBasketItem objects + * @return basket - String value in required format + */ + fun formatBasket(basketItems: List?): String { + if (basketItems.isNullOrEmpty()) { + Log.e(FormatterUtils.javaClass.simpleName, "Basket is Null or Empty") + return "" + } + return buildString { + for (basketItem in basketItems) { + // add prodId as '!i' + this.append("!i${basketItem.prodId}") + + // s - Sku id, only applies if you have skus. + if (!basketItem.sku.isNullOrEmpty()) { + this.append("'s${basketItem.sku}") + } + + // n + this.append("'n${basketItem.name}") + + // q + this.append("'q${basketItem.quantity}") + + // p - This should be the unit price per product and not total price. If the item is on sale, this is the unit sale price. + this.append("'p${basketItem.price}") + } + } + } + + /** + * 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 mapToUriBuilder(queryMap: MutableMap): Uri.Builder { + val uriBuilder = Uri.Builder() + queryMap.forEach { mapObject -> + uriBuilder.appendQueryParameter(mapObject.key, mapObject.value ?: "") + } + return uriBuilder + } + + /** + * 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): Uri.Builder { + val uriBuilder = Uri.Builder() + queryMap.forEach { mapObject -> + if(mapObject.value is String) { + uriBuilder.appendQueryParameter(mapObject.key, getUrlDecodeString(mapObject.value as String)) + } else if(mapObject.value is List<*>) { // check for fq + for(value in mapObject.value as List<*>) { + uriBuilder.appendQueryParameter(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/java/com/bloomreach/discovery/pixel/processpixel/PageViewPixelFormatter.kt b/src/main/java/com/bloomreach/discovery/pixel/processpixel/PageViewPixelFormatter.kt new file mode 100644 index 0000000..6154221 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/processpixel/PageViewPixelFormatter.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2022 Bloomreach + */ +package com.bloomreach.discovery.pixel.processpixel + +import com.bloomreach.discovery.pixel.model.PixelObject + +/** + * Class for creating query parameter as String for each Page View Pixel in required format + */ +internal class PageViewPixelFormatter { + + /** + * Method to generating query parameter String for Product Page View Pixel + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @param queryMap reference of Map where the values will be added + * @return Map with value in required format + */ + fun prepareProductPageViewQuery(pixelObject: PixelObject, queryMap: MutableMap): MutableMap { + queryMap.put("prod_id", pixelObject.prodId) + queryMap.put("prod_name", pixelObject.prodName) + if(pixelObject.prodSku != null) { + queryMap.put("sku", pixelObject.prodSku) + } + return queryMap + } + + /** + * Method to generating query parameter String for Content Page View Pixel + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @param queryMap reference of Map where the values will be added + * @return Map with value in required format + */ + fun prepareContentPageViewQuery(pixelObject: PixelObject, queryMap: MutableMap): MutableMap { + if (!pixelObject.catalogs.isNullOrEmpty()) { + queryMap.put("catalogs", FormatterUtils.formatCatalog(pixelObject.catalogs)) + } + queryMap.put("item_id", pixelObject.itemId) + queryMap.put("item_name", pixelObject.itemName) + return queryMap + } + + /** + * Method to generating query parameter String for Content Search Page View Pixel + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @param queryMap reference of Map where the values will be added + * @return Map with values in required format + */ + fun prepareContentSearchPageViewQuery(pixelObject: PixelObject, queryMap: MutableMap): MutableMap { + if (!pixelObject.catalogs.isNullOrEmpty()) { + queryMap.put("catalogs", FormatterUtils.formatCatalog(pixelObject.catalogs)) + } + queryMap.put("search_term", pixelObject.searchTerm) + return queryMap + } + + /** + * Method to generating query parameter String for Search Page View Pixel + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @param queryMap reference of Map where the values will be added + * @return Map with values in required format + */ + fun prepareSearchPageViewQuery(pixelObject: PixelObject, queryMap: MutableMap): MutableMap { + queryMap.put("search_term", pixelObject.searchTerm) + return queryMap + } + + /** + * Method to generating query parameter String for Category Page View Pixel + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @param queryMap reference of Map where the values will be added + * @return Map with values in required format + */ + fun prepareCategoryPageViewQuery(pixelObject: PixelObject, queryMap: MutableMap): MutableMap { + queryMap.put("cat", pixelObject.cat) + queryMap.put("cat_id", pixelObject.catId) + return queryMap + } + + /** + * Method to generating query parameter String for Conversion Page View Pixel + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @param queryMap reference of Map where the values will be added + * @return Map with values in required format + */ + fun prepareConversionPageViewQuery(pixelObject: PixelObject, queryMap: MutableMap): MutableMap { + queryMap.put("is_conversion", pixelObject.isConversion.toString()) + queryMap.put("basket_value", pixelObject.basketValue) + queryMap.put("order_id", pixelObject.orderId) + queryMap.put("basket", FormatterUtils.formatBasket(pixelObject.basket)) + return queryMap + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/processpixel/PixelProcessor.kt b/src/main/java/com/bloomreach/discovery/pixel/processpixel/PixelProcessor.kt new file mode 100644 index 0000000..eb0e39f --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/processpixel/PixelProcessor.kt @@ -0,0 +1,335 @@ +/* + * Copyright 2022 Bloomreach + */ +package com.bloomreach.discovery.pixel.processpixel + +import android.os.Build +import androidx.annotation.RequiresApi +import com.bloomreach.discovery.BuildConfig +import com.bloomreach.discovery.pixel.PixelTracker +import com.bloomreach.discovery.pixel.model.PageType +import com.bloomreach.discovery.pixel.model.PixelObject +import com.bloomreach.discovery.pixel.model.PixelType +import com.bloomreach.discovery.pixel.network.RestClient +import com.bloomreach.discovery.pixel.processpixel.FormatterUtils.generateRand +import com.bloomreach.discovery.pixel.submitpixel.ListenableQueue +import com.bloomreach.discovery.pixel.submitpixel.PixelQueue +import com.bloomreach.discovery.pixel.validator.PixelValidator +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Class for processing all the pixel and converting it in required query parameter string + */ +internal class PixelProcessor { + + //private val pixelJob: PixelJob = PixelJob() + private val restClient: RestClient = RestClient() + private val pixelValidator: PixelValidator = PixelValidator() + private val pageViewPixelFormatter = PageViewPixelFormatter() + + init { + PixelQueue.pixels.registerListener(object : + ListenableQueue.Listener> { + @RequiresApi(Build.VERSION_CODES.N) + override fun onElementAdded(element: MutableMap) { + performApi() + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun onElementRemoved(element: MutableMap) { + performApi() + } + }) + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun performApi() { + val nextElementFromQueue = PixelQueue.get() + if (nextElementFromQueue != null) { + val uriBuilder = FormatterUtils.mapToUriBuilder(nextElementFromQueue) + CoroutineScope(Dispatchers.IO).launch { + //API call + val response = restClient.submitPixel(uriBuilder) + PixelQueue.remove() + } + } + } + + /** + * Method to process Pixel triggered by Merchant app and convert it in required query parameter string + * @param pixelObject internal object which holds data for fields required to generate query parameter String + */ + fun processPixel(pixelObject: PixelObject) { + val queryMap = mutableMapOf() + + // process generic value + prepareGlobalQuery(pixelObject, queryMap) + + when (pixelObject.type) { + PixelType.PAGE_VIEW -> { + processPageViewPixel(pixelObject, queryMap) + } + PixelType.EVENT -> { + processEventPixel(pixelObject, queryMap) + } + } + + // Validate Pixel only when in DEBUG mode + if (PixelTracker.brPixel.debugMode) { + pixelValidator.validatePixel(queryMap) + } + + // add the processed Map to Queue for further process + PixelQueue.add(queryMap) + } + + fun processPixel(queryMap: MutableMap) { + + queryMap.put("acct_id", PixelTracker.brPixel.accountId) + + queryMap.put( + "cookie2", FormatterUtils.formatCookieValue( + PixelTracker.brPixel.uuid, + PixelTracker.brPixel.visitorType + ) + ) + + queryMap.put("rand", generateRand()) + + if (PixelTracker.brPixel.testData) { + queryMap.put("test_data", PixelTracker.brPixel.testData.toString()) + } + + queryMap.put( + "url", FormatterUtils.formatUrl( + PixelTracker.brPixel.baseUrl, + queryMap["ptype"] ?: "", + queryMap["title"] ?: "" + ) + ) + + // customer user id + if (!PixelTracker.brPixel.userId.isNullOrEmpty()) { + queryMap.put("user_id", PixelTracker.brPixel.userId) + } + + // customer tier + if (!PixelTracker.brPixel.customerTier.isNullOrEmpty()) { + queryMap.put("customer_tier", PixelTracker.brPixel.customerTier) + } + + // customer country present + if (!PixelTracker.brPixel.customerCountry.isNullOrEmpty()) { + queryMap.put("customer_country", PixelTracker.brPixel.customerCountry) + } + + // customer geo present + if (!PixelTracker.brPixel.customerGeo.isNullOrEmpty()) { + queryMap.put("customer_geo", PixelTracker.brPixel.customerGeo) + } + + // customer profile present + if (!PixelTracker.brPixel.customerProfile.isNullOrEmpty()) { + queryMap.put("customer_profile", PixelTracker.brPixel.customerProfile) + } + + // Validate Pixel only when in DEBUG mode + if (PixelTracker.brPixel.debugMode) { + pixelValidator.validatePixel(queryMap) + } + + //viewId + if (!PixelTracker.brPixel.viewId.isNullOrEmpty()) { + queryMap.put("view_id", PixelTracker.brPixel.viewId) + } + + //Currency + if (!PixelTracker.brPixel.currency.isNullOrEmpty()) { + queryMap.put("currency", PixelTracker.brPixel.currency) + } + + //DomainKey + if (!PixelTracker.brPixel.domainKey.isNullOrEmpty()) { + queryMap.put("domain_key", PixelTracker.brPixel.domainKey) + } + + // add the processed Map to Queue for further process + PixelQueue.add(queryMap) + } + + /** + * Method to process only Pixel View Pixel + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @return String value in required format + */ + private fun processPageViewPixel( + pixelObject: PixelObject, + queryMap: MutableMap + ): MutableMap { + // do processing based on pType for each PageView Pixels + when (pixelObject.pType) { + PageType.HOME_PAGE -> { + return queryMap + } + + PageType.PRODUCT_PAGE -> { + return pageViewPixelFormatter.prepareProductPageViewQuery(pixelObject, queryMap) + } + + PageType.CONTENT_PAGE -> { + return pageViewPixelFormatter.prepareContentPageViewQuery(pixelObject, queryMap) + } + + PageType.SEARCH_PAGE -> { + return if (pixelObject.catalogs.isNullOrEmpty()) { + pageViewPixelFormatter.prepareSearchPageViewQuery(pixelObject, queryMap) + } else { + pageViewPixelFormatter.prepareContentSearchPageViewQuery( + pixelObject, + queryMap + ) + } + } + + PageType.CATEGORY_PAGE -> { + return pageViewPixelFormatter.prepareCategoryPageViewQuery(pixelObject, queryMap) + } + + PageType.CONVERSION -> { + return pageViewPixelFormatter.prepareConversionPageViewQuery(pixelObject, queryMap) + } + +// PageType.OTHER_PAGE -> { +// return pageViewPixelFormatter.prepareConversionPageViewQuery(pixelObject, queryMap) +// } + else -> return queryMap + } + } + + /** + * Method to process only Event Pixel + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @param queryMap reference of Map where the values will be added + * @return Map with values in required format + */ + private fun processEventPixel( + pixelObject: PixelObject, + queryMap: MutableMap + ): MutableMap { + queryMap.put("group", pixelObject.group?.group) + + queryMap.put("etype", pixelObject.eType) + + queryMap.put("prod_id", pixelObject.prodId) + + queryMap.put("prod_name", pixelObject.prodName) + + if (!pixelObject.prodSku.isNullOrEmpty()) { + queryMap.put("sku", pixelObject.prodSku) + } + + if (!pixelObject.prodCollectionId.isNullOrEmpty()) { + queryMap.put("prod_collection_id", pixelObject.prodCollectionId) + } + + if (!pixelObject.searchTerm.isNullOrEmpty()) { + queryMap.put("q", pixelObject.searchTerm) + } + + if (!pixelObject.typedTerm.isNullOrEmpty()) { + queryMap.put("aq", pixelObject.typedTerm) + } + + if (!pixelObject.catalogs.isNullOrEmpty()) { + queryMap.put("catalogs", FormatterUtils.formatCatalog(pixelObject.catalogs)) + } + + return queryMap + } + + /** + * Method to process Global parameter required for Pixel and convert in required format + * @param pixelObject internal object which holds data for fields required to generate query parameter String + * @param queryMap reference of Map where the values will be added + * @return Map with values in required format + */ + private fun prepareGlobalQuery( + pixelObject: PixelObject, + queryMap: MutableMap + ): MutableMap { + queryMap.put("acct_id", PixelTracker.brPixel.accountId) + + queryMap.put( + "cookie2", FormatterUtils.formatCookieValue( + PixelTracker.brPixel.uuid, + PixelTracker.brPixel.visitorType + ) + ) + + queryMap.put("rand", generateRand()) + + queryMap.put("type", pixelObject.type.type) + + queryMap.put("ptype", pixelObject.pType.pType) + + if (PixelTracker.brPixel.testData) { + queryMap.put("test_data", PixelTracker.brPixel.testData.toString()) + } + + queryMap.put("title", pixelObject.title) + + queryMap.put( + "url", FormatterUtils.formatUrl( + PixelTracker.brPixel.baseUrl, + pixelObject.pType.pType, + pixelObject.title + ) + ) + + queryMap.put("ref", pixelObject.ref) + + // customer user id + if (!PixelTracker.brPixel.userId.isNullOrEmpty()) { + queryMap.put("user_id", PixelTracker.brPixel.userId) + } + + // customer tier + if (!PixelTracker.brPixel.customerTier.isNullOrEmpty()) { + queryMap.put("customer_tier", PixelTracker.brPixel.customerTier) + } + + // customer country present + if (!PixelTracker.brPixel.customerCountry.isNullOrEmpty()) { + queryMap.put("customer_country", PixelTracker.brPixel.customerCountry) + } + + // customer geo present + if (!PixelTracker.brPixel.customerGeo.isNullOrEmpty()) { + queryMap.put("customer_geo", PixelTracker.brPixel.customerGeo) + } + + // customer profile present + if (!PixelTracker.brPixel.customerProfile.isNullOrEmpty()) { + queryMap.put("customer_profile", PixelTracker.brPixel.customerProfile) + } + + //viewId + if (!PixelTracker.brPixel.viewId.isNullOrEmpty()) { + queryMap.put("view_id", PixelTracker.brPixel.viewId) + } + + //Currency + if (!PixelTracker.brPixel.currency.isNullOrEmpty()) { + queryMap.put("currency", PixelTracker.brPixel.currency) + } + + //DomainKey + if (!PixelTracker.brPixel.domainKey.isNullOrEmpty()) { + queryMap.put("domain_key", PixelTracker.brPixel.domainKey) + } + + return queryMap + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/submitpixel/ListenableQueue.kt b/src/main/java/com/bloomreach/discovery/pixel/submitpixel/ListenableQueue.kt new file mode 100644 index 0000000..7fc7f6d --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/submitpixel/ListenableQueue.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.submitpixel + +import android.os.Build +import androidx.annotation.RequiresApi +import java.util.* +import java.util.function.Consumer + +/** + * ListenableQueue Decorator pattern class to provide callbacks when item is added or removed from the Queue + */ +class ListenableQueue( + var delegate: Queue +) : AbstractQueue() { + private val listeners: MutableList> = ArrayList() + + fun registerListener(listener: Listener): ListenableQueue { + listeners.add(listener) + return this + } + + override fun iterator(): MutableIterator { + return delegate.iterator() + } + + override fun offer(e: E): Boolean { + // here, add the element and if Queue was empty then notify listeners + if (delegate.size == 0) { + return if (delegate.offer(e)) { + listeners.forEach(Consumer { listener: Listener -> + listener.onElementAdded(e) + }) + true + } else { + false + } + } + return delegate.offer(e) + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun poll(): E? { + // here, remove the element and if Queue was not empty then notify listeners + val element = delegate.poll() + if (delegate.size > 0) { + listeners.forEach(Consumer { listener: Listener -> + listener.onElementRemoved(element) + }) + } + return element + } + + override fun peek(): E? { + return delegate.peek() + } + + override val size: Int + get() = delegate.size + + interface Listener { + fun onElementAdded(element: E) + fun onElementRemoved(element: E) + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/submitpixel/PixelJob.kt b/src/main/java/com/bloomreach/discovery/pixel/submitpixel/PixelJob.kt new file mode 100644 index 0000000..102dff9 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/submitpixel/PixelJob.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.submitpixel + +import com.bloomreach.discovery.pixel.processpixel.FormatterUtils +import com.bloomreach.discovery.pixel.network.RestClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * PixelJob class to trigger submit api call continuously if Queue is not empty + */ +internal class PixelJob { + private val restClient: RestClient = RestClient() + var isInProgress = false + + /** + * Method to keep continuously push the Pixels from Queue to API call and removing from Queue + */ + fun pushPixels() { + if (PixelQueue.get() != null) { + isInProgress = true + val uriBuilder = FormatterUtils.mapToUriBuilder(PixelQueue.get()!!) + CoroutineScope(Dispatchers.IO).launch { + val response = restClient.submitPixel(uriBuilder) + //Log.i("PixelJob", response.toString()) + // PixelQueue.remove() + if (PixelQueue.get() != null) { + // if Pixels are lying in Queue, pick the next pixel. + pushPixels() + } else { + isInProgress = false + } + } + } else { + isInProgress = true + } + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/submitpixel/PixelQueue.kt b/src/main/java/com/bloomreach/discovery/pixel/submitpixel/PixelQueue.kt new file mode 100644 index 0000000..b909082 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/submitpixel/PixelQueue.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.submitpixel + +import android.os.Build +import androidx.annotation.RequiresApi +import java.util.* + +/** + * PixelQueue class to hold pixels query parameter map to be sent to PIXEL API + */ +internal object PixelQueue { + val pixels: ListenableQueue> = + ListenableQueue(LinkedList()) + + /** + * Method to add Pixel to Queue + * @param queryMap Map of query parameter + */ + fun add(queryMap: MutableMap) { + // add to Queue + pixels.add(queryMap) + } + + /** + * Method to pick Pixel from Queue + * @return Map of query parameters + */ + fun get(): MutableMap? { + // Get the element at the front of the Queue (without removing it) + // if the Queue is empty, element() throws NoSuchElementException while peek() returns null. + return pixels.peek() + } + + /** + * Method to remove Pixel from Queue + */ + @RequiresApi(Build.VERSION_CODES.N) + fun remove() { + //remove an element from the Queue (dequeue operation) + //poll() method is similar to remove() (dequeue operation), but it returns null if the Queue is empty instead of throwing an exception. + pixels.poll() + } +} \ No newline at end of file diff --git a/src/main/java/com/bloomreach/discovery/pixel/validator/PixelValidator.kt b/src/main/java/com/bloomreach/discovery/pixel/validator/PixelValidator.kt new file mode 100644 index 0000000..cf16dc4 --- /dev/null +++ b/src/main/java/com/bloomreach/discovery/pixel/validator/PixelValidator.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2022 Bloomreach + */ + +package com.bloomreach.discovery.pixel.validator + +import android.util.Log +import com.bloomreach.discovery.pixel.PixelTracker +import com.bloomreach.discovery.pixel.model.validator.PixelValidatorBody +import com.bloomreach.discovery.pixel.model.validator.response.PixelValidatorResponse +import com.bloomreach.discovery.pixel.network.RestClient +import com.bloomreach.discovery.pixel.processpixel.FormatterUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * PixelValidator class to format Pixel Validator request and perform the API call and format the response + */ +internal class PixelValidator { + private val restClient: RestClient = RestClient() + private val protocol = "https" + private val path = "pix.gif" + + /** + * Method to format the Pixel validator request in required format and perform validator API call + * + * @param queryMap Map of pixel values in required format + */ + fun validatePixel(queryMap: MutableMap) { + + // operations to form post body + val uriBuilder = FormatterUtils.mapToUriBuilder(queryMap) + val query = uriBuilder.build().toString() + uriBuilder.scheme(protocol) + uriBuilder.authority(PixelTracker.brPixel.pixelUrlByRegion) + uriBuilder.appendPath(path) + + val postBody = PixelValidatorBody( + source = uriBuilder.build().toString(), + protocol = protocol, + host = PixelTracker.brPixel.pixelUrlByRegion, + port = "", + query = query, + params = queryMap, + file = path, + hash = "", + path = "/$path", + relative = "/$path$query", + segments = listOf(path) + ) + +// for Rest Call + CoroutineScope(Dispatchers.IO).launch { + // Validate Pixel REST call + val response = restClient.validatePixel(postBody) + printPixelValidatorResponse(response) + } + } + + /** + * Method to format the Pixel validator response object in required format and print it to logs. + * + * @param response PixelValidatorResponse received from Pixel validator call + */ + private fun printPixelValidatorResponse(response: PixelValidatorResponse?) { + if (response == null) { + return + } + + if (!response.errors.isNullOrEmpty()) { + Log.v( + "Pixel Validator", + "\n==========PIXEL VALIDATOR ERROR========= \n\n${formResponseString(response)} \n\n" + ) + } else if (!response.warns.isNullOrEmpty()) { + Log.v( + "Pixel Validator", + "\n==========PIXEL VALIDATOR WARNING========= \n\n${formResponseString(response)} \n\n" + ) + } else { + Log.v( + "Pixel Validator", + "\n==========PIXEL VALIDATOR SUCCESS========= \n\n${formResponseString(response)} \n\n" + ) + } + } + + /** + * Method to format the Pixel validator response object in required format + * + * @param response PixelValidatorResponse received from Pixel validator call + */ + private fun formResponseString(response: PixelValidatorResponse): String { + return buildString { + this.append("Pixel Name: ${response.displayName}\n\n") + + if (!response.errors.isNullOrEmpty()) { + this.append("Error in Parameters ==>") + for (error in response.errors) { + this.append("${error.name}: ${error.value}\n") + this.append("Description: ${error.description}\n\n") + } + } + + if (!response.warns.isNullOrEmpty()) { + this.append("Warning in Parameters ==>\n") + for (warn in response.warns) { + this.append("${warn.name}: ${warn.value}\n") + this.append("Description: ${warn.description}\n\n") + } + } + + if (!response.success.isNullOrEmpty()) { + this.append("Success Parameters ==>\n") + for (success in response.success) { + this.append("${success.name}: ${success.value}\n") + } + } + } + } + + +} \ No newline at end of file