diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java index b8b62f97..227de356 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java @@ -12,1216 +12,15 @@ package com.adobe.marketing.mobile.optimize; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import com.adobe.marketing.mobile.AdobeCallbackWithError; -import com.adobe.marketing.mobile.AdobeError; -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.EventType; -import com.adobe.marketing.mobile.Extension; -import com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.MobileCore; -import com.adobe.marketing.mobile.SharedStateResolution; -import com.adobe.marketing.mobile.SharedStateResult; -import com.adobe.marketing.mobile.SharedStateStatus; -import com.adobe.marketing.mobile.services.Log; -import com.adobe.marketing.mobile.util.DataReader; -import com.adobe.marketing.mobile.util.DataReaderException; -import com.adobe.marketing.mobile.util.SerialWorkDispatcher; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -class OptimizeExtension extends Extension { +import com.adobe.marketing.mobile.ExtensionV2; +import com.adobe.marketing.mobile.ExtensionV2Delegate; - private static final String SELF_TAG = "OptimizeExtension"; - - // Concurrent Map containing the cached propositions returned in various - // personalization:decisions events - // for the same Edge personalization request. - // This is accessed from multiple threads. - private Map cachedPropositions = new ConcurrentHashMap<>(); - - // Concurrent Map containing propositions simulated for preview and cached in-memory in the SDK - private Map previewCachedPropositions = - new ConcurrentHashMap<>(); - - // Events dispatcher used to maintain the processing order of update and get propositions - // events. - // It ensures any update propositions requests issued before a get propositions call are - // completed - // and the get propositions request is fulfilled from the latest cached content. - private SerialWorkDispatcher eventsDispatcher = - new SerialWorkDispatcher( - "OptimizeEventsDispatcher", - new SerialWorkDispatcher.WorkHandler() { - @Override - public boolean doWork(final Event event) { - if (OptimizeUtils.isGetEvent(event)) { - handleGetPropositions(event); - } else if (event.getType() - .equalsIgnoreCase(OptimizeConstants.EventType.EDGE)) { - return !updateRequestEventIdsInProgress.containsKey( - event.getUniqueIdentifier()); - } - return true; - } - }); - - // Concurrent Map containing the update event IDs (and corresponding requested scopes) for Edge - // events that haven't yet received an Edge completion response. - // This is accessed from multiple threads. - private final Map> updateRequestEventIdsInProgress = - new ConcurrentHashMap<>(); - - // Concurrent Map to accumulate propositions returned in various personalization:decisions - // events - // for the same Edge personalization request. - // This is accessed from multiple threads. - private final Map propositionsInProgress = - new ConcurrentHashMap<>(); - - // List containing the schema strings for the proposition items supported by the SDK, sent in - // the personalization query request. - static final List supportedSchemas = - Arrays.asList( - // Target schemas - OptimizeConstants.JsonValues.SCHEMA_TARGET_HTML, - OptimizeConstants.JsonValues.SCHEMA_TARGET_JSON, - OptimizeConstants.JsonValues.SCHEMA_TARGET_DEFAULT, - - // Offer Decisioning schemas - OptimizeConstants.JsonValues.SCHEMA_OFFER_HTML, - OptimizeConstants.JsonValues.SCHEMA_OFFER_JSON, - OptimizeConstants.JsonValues.SCHEMA_OFFER_IMAGE, - OptimizeConstants.JsonValues.SCHEMA_OFFER_TEXT); - - // List containing recoverable network error codes being retried by Edge Network Service - private static final List recoverableNetworkErrorCodes = - Arrays.asList( - OptimizeConstants.HTTPResponseCodes.clientTimeout, - OptimizeConstants.HTTPResponseCodes.tooManyRequests, - OptimizeConstants.HTTPResponseCodes.badGateway, - OptimizeConstants.HTTPResponseCodes.serviceUnavailable, - OptimizeConstants.HTTPResponseCodes.gatewayTimeout); - - // Map containing the update event IDs and corresponding errors as received from Edge SDK - private static final Map updateRequestEventIdsErrors = - new ConcurrentHashMap<>(); - - /** - * Constructor for {@code OptimizeExtension}. - * - *

It is invoked during the extension registration to retrieve the extension's details such - * as name and version. The following {@link Event} listeners are registered during the process. - * - *

    - *
  • Listener for {@code Event} type {@value OptimizeConstants.EventType#OPTIMIZE} and - * source {@value OptimizeConstants.EventSource#REQUEST_CONTENT} Listener for {@code - * Event} type {@value OptimizeConstants.EventType#EDGE} and source {@value - * OptimizeConstants.EventSource#EDGE_PERSONALIZATION_DECISIONS} Listener for {@code - * Event} type {@value OptimizeConstants.EventType#EDGE} and source {@value - * OptimizeConstants.EventSource#ERROR_RESPONSE_CONTENT} Listener for {@code Event} type - * {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#REQUEST_RESET} Listener for {@code Event} type {@value - * OptimizeConstants.EventType#GENERIC_IDENTITY} and source {@value - * OptimizeConstants.EventSource#REQUEST_RESET} Listener for {@code Event} type {@value - * OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#CONTENT_COMPLETE} Listener for {@code Event} type {@value - * EventType#SYSTEM} and source {@value OptimizeConstants.EventSource#DEBUG} - *
- * - * @param extensionApi {@link ExtensionApi} instance. - */ - protected OptimizeExtension(final ExtensionApi extensionApi) { - super(extensionApi); - } +class OptimizeExtension extends ExtensionV2Delegate { + @NonNull @Override - protected void onRegistered() { - getApi().registerEventListener( - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.REQUEST_CONTENT, - this::handleOptimizeRequestContent); - - getApi().registerEventListener( - OptimizeConstants.EventType.EDGE, - OptimizeConstants.EventSource.EDGE_PERSONALIZATION_DECISIONS, - this::handleEdgeResponse); - - getApi().registerEventListener( - OptimizeConstants.EventType.EDGE, - OptimizeConstants.EventSource.ERROR_RESPONSE_CONTENT, - this::handleEdgeErrorResponse); - - getApi().registerEventListener( - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.REQUEST_RESET, - this::handleClearPropositions); - - // Register listener - Mobile Core `resetIdentities()` API dispatches generic identity - // request reset event. - getApi().registerEventListener( - OptimizeConstants.EventType.GENERIC_IDENTITY, - OptimizeConstants.EventSource.REQUEST_RESET, - this::handleClearPropositions); - - getApi().registerEventListener( - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.CONTENT_COMPLETE, - this::handleUpdatePropositionsCompleted); - - getApi().registerEventListener( - EventType.SYSTEM, - OptimizeConstants.EventSource.DEBUG, - this::handleDebugEvent); - - eventsDispatcher.start(); - } - - @Override - public boolean readyForEvent(@NonNull final Event event) { - if (OptimizeConstants.EventType.OPTIMIZE.equalsIgnoreCase(event.getType()) - && OptimizeConstants.EventSource.REQUEST_CONTENT.equalsIgnoreCase( - event.getSource())) { - SharedStateResult configurationSharedState = - getApi().getSharedState( - OptimizeConstants.Configuration.EXTENSION_NAME, - event, - false, - SharedStateResolution.ANY); - return configurationSharedState != null - && configurationSharedState.getStatus() == SharedStateStatus.SET; - } - return true; - } - - /** - * Retrieve the extension name. - * - * @return {@link String} containing the unique name for this extension. - */ - @NonNull @Override - protected String getName() { - return OptimizeConstants.EXTENSION_NAME; - } - - /** - * Retrieve the extension version. - * - * @return {@link String} containing the current installed version of this extension. - */ - @NonNull @Override - protected String getVersion() { - return OptimizeConstants.EXTENSION_VERSION; - } - - /** - * Retrieve the friendly name. - * - * @return {@link String} containing the friendly name for this extension. - */ - @NonNull @Override - protected String getFriendlyName() { - return OptimizeConstants.FRIENDLY_NAME; - } - - /** - * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#REQUEST_CONTENT}. - * - *

This method handles the event based on the value of {@value - * OptimizeConstants.EventDataKeys#REQUEST_TYPE} in the event data of current {@code event} - * - * @param event incoming {@link Event} object to be processed. - */ - void handleOptimizeRequestContent(@NonNull final Event event) { - if (OptimizeUtils.isNullOrEmpty(event.getEventData())) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleOptimizeRequestContent - Ignoring the Optimize request event, either" - + " event is null or event data is null/ empty."); - return; - } - - final Map eventData = event.getEventData(); - final String requestType = - DataReader.optString(eventData, OptimizeConstants.EventDataKeys.REQUEST_TYPE, ""); - - switch (requestType) { - case OptimizeConstants.EventDataValues.REQUEST_TYPE_UPDATE: - handleUpdatePropositions(event); - break; - case OptimizeConstants.EventDataValues.REQUEST_TYPE_GET: - try { - // Fetch decision scopes from the event - List> decisionScopesData = - DataReader.getTypedListOfMap( - Object.class, - eventData, - OptimizeConstants.EventDataKeys.DECISION_SCOPES); - List eventDecisionScopes = - retrieveValidDecisionScopes(decisionScopesData); - - if (OptimizeUtils.isNullOrEmpty(eventDecisionScopes)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleOptimizeRequestContent - Cannot process the get propositions" - + " request event, provided list of decision scopes has no" - + " valid scope."); - getApi().dispatch( - createResponseEventWithError( - event, AdobeError.UNEXPECTED_ERROR)); - return; - } - - // Fetch propositions for the decision scopes from the cache - Map fetchedPropositions = new HashMap<>(); - for (DecisionScope scope : eventDecisionScopes) { - if (cachedPropositions.containsKey(scope)) { - fetchedPropositions.put(scope, cachedPropositions.get(scope)); - } - } - - // Check if all scopes are cached and none are in progress - boolean anyScopeInProgress = false; - HashSet scopesInProgress = new HashSet<>(); - for (List updatingScope : - updateRequestEventIdsInProgress.values()) { - scopesInProgress.addAll(updatingScope); - } - for (DecisionScope scope : eventDecisionScopes) { - if (scopesInProgress.contains(scope)) { - anyScopeInProgress = true; - break; - } - } - - if ((fetchedPropositions.size() == eventDecisionScopes.size()) - && !anyScopeInProgress) { - Log.trace( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleOptimizeRequestContent - All scopes are cached and none are" - + " in progress, dispatching event directly."); - - // Dispatch the event directly - handleGetPropositions(event); - } else { - Log.trace( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleOptimizeRequestContent - Scopes are not fully cached or are" - + " in progress, adding event to dispatcher."); - eventsDispatcher.offer(event); - } - break; - } catch (final Exception e) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleOptimizeRequestContent - Failed to process get propositions" - + " request event due to an exception (%s)!", - e.getLocalizedMessage()); - } - break; - case OptimizeConstants.EventDataValues.REQUEST_TYPE_TRACK: - handleTrackPropositions(event); - break; - default: - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleOptimizeRequestContent - Ignoring the Optimize request event," - + " provided request type (%s) is not handled by this extension.", - requestType); - break; - } - } - - /** - * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#REQUEST_CONTENT}. - * - *

This method dispatches an event to the Edge network extension to send personalization - * query request to the Experience Edge network. The dispatched event contains additional XDM - * and/ or free-form data, read from the incoming event, to be attached to the Edge request. - * - * @param event incoming {@link Event} object to be processed. - */ - void handleUpdatePropositions(@NonNull final Event event) { - final Map eventData = event.getEventData(); - - final Map configData = retrieveConfigurationSharedState(event); - if (OptimizeUtils.isNullOrEmpty(configData)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleUpdatePropositions - Cannot process the update propositions request" - + " event, Configuration shared state is not available."); - return; - } - - try { - final List> decisionScopesData = - DataReader.getTypedListOfMap( - Object.class, - eventData, - OptimizeConstants.EventDataKeys.DECISION_SCOPES); - final List validScopes = retrieveValidDecisionScopes(decisionScopesData); - if (OptimizeUtils.isNullOrEmpty(validScopes)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleUpdatePropositions - Cannot process the update propositions request" - + " event, provided list of decision scopes has no valid scope."); - return; - } - - final Map edgeEventData = new HashMap<>(); - - // Add query - final Map queryPersonalization = new HashMap<>(); - queryPersonalization.put(OptimizeConstants.JsonKeys.SCHEMAS, supportedSchemas); - - final List validScopeNames = new ArrayList<>(); - for (final DecisionScope scope : validScopes) { - validScopeNames.add(scope.getName()); - } - queryPersonalization.put(OptimizeConstants.JsonKeys.DECISION_SCOPES, validScopeNames); - - final Map query = new HashMap<>(); - query.put(OptimizeConstants.JsonKeys.QUERY_PERSONALIZATION, queryPersonalization); - edgeEventData.put(OptimizeConstants.JsonKeys.QUERY, query); - - // Add xdm - final Map xdm = new HashMap<>(); - if (eventData.containsKey(OptimizeConstants.EventDataKeys.XDM)) { - final Map inputXdm = - DataReader.getTypedMap( - Object.class, eventData, OptimizeConstants.EventDataKeys.XDM); - if (!OptimizeUtils.isNullOrEmpty(inputXdm)) { - xdm.putAll(inputXdm); - } - } - xdm.put( - OptimizeConstants.JsonKeys.EXPERIENCE_EVENT_TYPE, - OptimizeConstants.JsonValues.EE_EVENT_TYPE_PERSONALIZATION); - edgeEventData.put(OptimizeConstants.JsonKeys.XDM, xdm); - - // Add data - final Map data = new HashMap<>(); - if (eventData.containsKey(OptimizeConstants.EventDataKeys.DATA)) { - final Map inputData = - DataReader.getTypedMap( - Object.class, eventData, OptimizeConstants.EventDataKeys.DATA); - if (!OptimizeUtils.isNullOrEmpty(inputData)) { - data.putAll(inputData); - edgeEventData.put(OptimizeConstants.JsonKeys.DATA, data); - } - } - - // Add the flag to request sendCompletion - final Map request = new HashMap<>(); - request.put(OptimizeConstants.JsonKeys.REQUEST_SEND_COMPLETION, true); - edgeEventData.put(OptimizeConstants.JsonKeys.REQUEST, request); - - // Add override datasetId - if (configData.containsKey( - OptimizeConstants.Configuration.OPTIMIZE_OVERRIDE_DATASET_ID)) { - final String overrideDatasetId = - DataReader.getString( - configData, - OptimizeConstants.Configuration.OPTIMIZE_OVERRIDE_DATASET_ID); - if (!OptimizeUtils.isNullOrEmpty(overrideDatasetId)) { - edgeEventData.put(OptimizeConstants.JsonKeys.DATASET_ID, overrideDatasetId); - } - } - - final Event edgeEvent = - new Event.Builder( - OptimizeConstants.EventNames.EDGE_PERSONALIZATION_REQUEST, - OptimizeConstants.EventType.EDGE, - OptimizeConstants.EventSource.REQUEST_CONTENT) - .setEventData(edgeEventData) - .chainToParentEvent(event) - .build(); - - // In AEP Response Event handle, `requestEventId` corresponds to the unique identifier - // for the Edge request. - // Storing the request event unique identifier to compare and process only the - // anticipated response in the extension. - updateRequestEventIdsInProgress.put(edgeEvent.getUniqueIdentifier(), validScopes); - - // add the Edge event to update propositions in the events queue. - eventsDispatcher.offer(edgeEvent); - long timeoutMillis = ConfigUtils.retrieveOptimizeRequestTimeout(event, configData); - MobileCore.dispatchEventWithResponseCallback( - edgeEvent, - timeoutMillis, - new AdobeCallbackWithError() { - @Override - public void fail(final AdobeError error) { - // response event failed or timed out, remove this event's unique - // identifier from the requested event IDs dictionary and kick-off - // queue. - updateRequestEventIdsInProgress.remove(edgeEvent.getUniqueIdentifier()); - propositionsInProgress.clear(); - - AEPOptimizeError aepOptimizeError; - if (error == AdobeError.CALLBACK_TIMEOUT) { - aepOptimizeError = AEPOptimizeError.Companion.getTimeoutError(); - } else { - aepOptimizeError = AEPOptimizeError.Companion.getUnexpectedError(); - } - - getApi().dispatch( - createResponseEventWithError(event, aepOptimizeError)); - - eventsDispatcher.resume(); - } - - @Override - public void call(final Event callbackEvent) { - final String requestEventId = - OptimizeUtils.getRequestEventId(callbackEvent); - if (OptimizeUtils.isNullOrEmpty(requestEventId)) { - fail(AdobeError.UNEXPECTED_ERROR); - return; - } - - final Map responseEventData = new HashMap<>(); - AEPOptimizeError aepOptimizeError = - updateRequestEventIdsErrors.get(requestEventId); - if (aepOptimizeError != null) { - responseEventData.put( - OptimizeConstants.EventDataKeys.RESPONSE_ERROR, - aepOptimizeError.toEventData()); - } - - final List> propositionsList = new ArrayList<>(); - - for (Map.Entry entry : - propositionsInProgress.entrySet()) { - OptimizeProposition optimizeProposition = entry.getValue(); - propositionsList.add(optimizeProposition.toEventData()); - } - - responseEventData.put( - OptimizeConstants.EventDataKeys.PROPOSITIONS, propositionsList); - - final Event responseEvent = - new Event.Builder( - OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.RESPONSE_CONTENT) - .setEventData(responseEventData) - .inResponseToEvent(event) - .build(); - - getApi().dispatch(responseEvent); - - final Event updateCompleteEvent = - new Event.Builder( - OptimizeConstants.EventNames - .OPTIMIZE_UPDATE_COMPLETE, - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.CONTENT_COMPLETE) - .setEventData( - new HashMap() { - { - put( - OptimizeConstants.EventDataKeys - .COMPLETED_UPDATE_EVENT_ID, - requestEventId); - } - }) - .chainToParentEvent(event) - .build(); - - getApi().dispatch(updateCompleteEvent); - } - }); - } catch (final Exception e) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleUpdatePropositions - Failed to process update propositions request event" - + " due to an exception (%s)!", - e.getLocalizedMessage()); - } - } - - /** - * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#CONTENT_COMPLETE}. - * - *

The event is dispatched internally upon receiving an Edge content complete response for an - * update propositions request. - * - * @param event incoming {@link Event} object to be processed. - */ - void handleUpdatePropositionsCompleted(@NonNull final Event event) { - try { - final String requestCompletedForEventId = - DataReader.getString( - event.getEventData(), - OptimizeConstants.EventDataKeys.COMPLETED_UPDATE_EVENT_ID); - if (OptimizeUtils.isNullOrEmpty(requestCompletedForEventId)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleUpdatePropositionsCompleted - Ignoring Optimize complete event," - + " event Id for the completed event is not present in event data"); - return; - } - - final List requestedScopes = - updateRequestEventIdsInProgress.get(requestCompletedForEventId); - if (OptimizeUtils.isNullOrEmpty(requestedScopes)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleUpdatePropositionsCompleted - Ignoring Optimize complete event," - + " event Id is not being tracked for completion as requested scopes is" - + " null or empty."); - return; - } - - // Update propositions in cache - updateCachedPropositions(requestedScopes); - - // remove completed event's ID from the request event IDs dictionary. - updateRequestEventIdsInProgress.remove(requestCompletedForEventId); - } catch (final DataReaderException e) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleUpdatePropositionsCompleted - Cannot process the update propositions" - + " complete event due to an exception (%s)!", - e.getLocalizedMessage()); - } finally { - propositionsInProgress.clear(); - - // Resume events dispatcher processing after update propositions request is completed. - eventsDispatcher.resume(); - } - } - - /** - * Updates the in-memory propositions cache with the returned propositions. - * - *

Any requested scopes for which no propositions are returned in personalization: decisions - * events are removed from the cache. - * - * @param requestedScopes a {@code List} for which propositions are requested. - */ - private void updateCachedPropositions(@NonNull final List requestedScopes) { - // update cache with accumulated propositions - cachedPropositions.putAll(propositionsInProgress); - - // remove cached propositions for requested scopes for which no propositions are returned. - final List returnedScopes = new ArrayList<>(propositionsInProgress.keySet()); - final List scopesToRemove = new ArrayList<>(requestedScopes); - scopesToRemove.removeAll(returnedScopes); - - for (final DecisionScope scope : scopesToRemove) { - cachedPropositions.remove(scope); - } - } - - /** - * Handles the event with type {@value OptimizeConstants.EventType#EDGE} and source {@value - * OptimizeConstants.EventSource#EDGE_PERSONALIZATION_DECISIONS}. - * - *

This method caches the propositions, returned in the Edge response, in the SDK. It also - * dispatches a personalization notification event with the received propositions. - * - * @param event incoming {@link Event} object to be processed. - */ - void handleEdgeResponse(@NonNull final Event event) { - try { - final Map eventData = event.getEventData(); - final String requestEventId = OptimizeUtils.getRequestEventId(event); - - if (!OptimizeUtils.isPersonalizationDecisionsResponse(event) - || OptimizeUtils.isNullOrEmpty(requestEventId) - || !updateRequestEventIdsInProgress.containsKey(requestEventId)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeResponse - Ignoring Edge event, either handle type is not" - + " personalization:decisions, or the response isn't intended for this" - + " extension."); - propositionsInProgress.clear(); - return; - } - - final List> payload = - DataReader.getTypedListOfMap( - Object.class, eventData, OptimizeConstants.Edge.PAYLOAD); - if (OptimizeUtils.isNullOrEmpty(payload)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeResponse - Cannot process the Edge personalization:decisions" - + " event, propositions list is either null or empty in the Edge" - + " response."); - return; - } - - final Map propositionsMap = new HashMap<>(); - for (final Map propositionData : payload) { - final OptimizeProposition optimizeProposition = - OptimizeProposition.fromEventData(propositionData); - if (optimizeProposition != null - && !OptimizeUtils.isNullOrEmpty(optimizeProposition.getOffers())) { - final DecisionScope scope = new DecisionScope(optimizeProposition.getScope()); - propositionsMap.put(scope, optimizeProposition); - } - } - - if (OptimizeUtils.isNullOrEmpty(propositionsMap)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeResponse - Cannot process the Edge personalization:decisions" - + " event, no propositions with valid offers are present in the Edge" - + " response."); - return; - } - - // accumulate propositions in in-progress propositions dictionary - propositionsInProgress.putAll(propositionsMap); - - final List> propositionsList = new ArrayList<>(); - for (final OptimizeProposition optimizeProposition : propositionsMap.values()) { - propositionsList.add(optimizeProposition.toEventData()); - } - final Map notificationData = new HashMap<>(); - notificationData.put(OptimizeConstants.EventDataKeys.PROPOSITIONS, propositionsList); - - final Event edgeEvent = - new Event.Builder( - OptimizeConstants.EventNames.OPTIMIZE_NOTIFICATION, - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.NOTIFICATION) - .setEventData(notificationData) - .build(); - - // Dispatch notification event - getApi().dispatch(edgeEvent); - } catch (final Exception e) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeResponse - Cannot process the Edge personalization:decisions event" - + " due to an exception (%s)!", - e.getLocalizedMessage()); - } - } - - /** - * Handles the event with type {@value OptimizeConstants.EventType#EDGE} and source {@value - * OptimizeConstants.EventSource#ERROR_RESPONSE_CONTENT}. - * - *

This method logs the error information, returned in Edge response, specifying error type - * along with a detail message. - * - * @param event incoming {@link Event} object to be processed. - */ - void handleEdgeErrorResponse(@NonNull final Event event) { - try { - final Map eventData = event.getEventData(); - final String requestEventId = OptimizeUtils.getRequestEventId(event); - - if (!OptimizeUtils.isEdgeErrorResponseContent(event) - || OptimizeUtils.isNullOrEmpty(requestEventId) - || !updateRequestEventIdsInProgress.containsKey(requestEventId)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeResponse - Ignoring Edge event, either handle type is not edge" - + " error response content, or the response isn't intended for this" - + " extension."); - return; - } - - if (OptimizeUtils.isNullOrEmpty(event.getEventData())) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeErrorResponse - Ignoring the Edge error response event, either" - + " event is null or event data is null/ empty."); - return; - } - - final String errorType = - DataReader.optString( - eventData, - OptimizeConstants.Edge.ErrorKeys.TYPE, - OptimizeConstants.ERROR_UNKNOWN); - final int errorStatus = - DataReader.optInt( - eventData, - OptimizeConstants.Edge.ErrorKeys.STATUS, - OptimizeConstants.UNKNOWN_STATUS); - final String errorTitle = - DataReader.optString( - eventData, - OptimizeConstants.Edge.ErrorKeys.TITLE, - OptimizeConstants.ERROR_UNKNOWN); - final String errorDetail = - DataReader.optString( - eventData, - OptimizeConstants.Edge.ErrorKeys.DETAIL, - OptimizeConstants.ERROR_UNKNOWN); - final Map errorReport = - DataReader.optTypedMap( - Object.class, - eventData, - OptimizeConstants.Edge.ErrorKeys.REPORT, - new HashMap<>()); - - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeErrorResponse - Decisioning Service error! Error type: (%s),\n" - + "title: (%s),\n" - + "detail: (%s),\n" - + "status: (%s),\n" - + "report: (%s)", - errorType, - errorTitle, - errorDetail, - errorStatus, - errorReport); - - // Check if the errorStatus is in the list of recoverable error codes - if (recoverableNetworkErrorCodes.contains(errorStatus)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "Recoverable error encountered: Status %d", - errorStatus); - return; - } else { - AEPOptimizeError aepOptimizeError = - new AEPOptimizeError( - errorType, errorStatus, errorTitle, errorDetail, errorReport, null); - updateRequestEventIdsErrors.put(requestEventId, aepOptimizeError); - } - } catch (final Exception e) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleEdgeResponse - Cannot process the Edge Error Response event" - + " due to an exception (%s)!", - e.getLocalizedMessage()); - } - } - - /** - * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#REQUEST_CONTENT}. - * - *

This method caches the propositions, returned in the Edge response, in the SDK. It also - * dispatches an optimize response event with the propositions for the requested decision - * scopes. - * - * @param event incoming {@link Event} object to be processed. - */ - void handleGetPropositions(@NonNull final Event event) { - final Map eventData = event.getEventData(); - - try { - final List> decisionScopesData = - DataReader.getTypedListOfMap( - Object.class, - eventData, - OptimizeConstants.EventDataKeys.DECISION_SCOPES); - final List validScopes = retrieveValidDecisionScopes(decisionScopesData); - if (OptimizeUtils.isNullOrEmpty(validScopes)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleGetPropositions - Cannot process the get propositions request event," - + " provided list of decision scopes has no valid scope."); - getApi().dispatch(createResponseEventWithError(event, AdobeError.UNEXPECTED_ERROR)); - return; - } - - final List> propositionsList = new ArrayList<>(); - for (final DecisionScope scope : validScopes) { - if (cachedPropositions.containsKey(scope)) { - final OptimizeProposition optimizeProposition = cachedPropositions.get(scope); - propositionsList.add(optimizeProposition.toEventData()); - } - } - - final List> previewPropositionsList = new ArrayList<>(); - for (final DecisionScope scope : validScopes) { - if (previewCachedPropositions.containsKey(scope)) { - final OptimizeProposition optimizeProposition = - previewCachedPropositions.get(scope); - previewPropositionsList.add(optimizeProposition.toEventData()); - } - } - - final Map responseEventData = new HashMap<>(); - - if (!previewPropositionsList.isEmpty()) { - Log.debug(OptimizeConstants.LOG_TAG, SELF_TAG, "Preview Mode is enabled."); - responseEventData.put( - OptimizeConstants.EventDataKeys.PROPOSITIONS, previewPropositionsList); - } else { - responseEventData.put( - OptimizeConstants.EventDataKeys.PROPOSITIONS, propositionsList); - } - - final Event responseEvent = - new Event.Builder( - OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.RESPONSE_CONTENT) - .setEventData(responseEventData) - .inResponseToEvent(event) - .build(); - - getApi().dispatch(responseEvent); - - } catch (final Exception e) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleGetPropositions - Failed to process get propositions request event due" - + " to an exception (%s)!", - e.getLocalizedMessage()); - getApi().dispatch(createResponseEventWithError(event, AdobeError.UNEXPECTED_ERROR)); - } - } - - /** - * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#REQUEST_CONTENT}. - * - *

This method dispatches an event to the Edge network extension to send proposition - * interactions information to the Experience Edge network. The dispatched event may contain an - * override {@code datasetId} indicating the dataset which will be used for storing the - * Experience Events sent to the Edge network. - * - * @param event incoming {@link Event} object to be processed. - */ - void handleTrackPropositions(@NonNull final Event event) { - final Map eventData = event.getEventData(); - - final Map configData = retrieveConfigurationSharedState(event); - if (OptimizeUtils.isNullOrEmpty(configData)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleTrackPropositions - Cannot process the track propositions request event," - + " Configuration shared state is not available."); - return; - } - - try { - final Map propositionInteractionsXdm = - DataReader.getTypedMap( - Object.class, - eventData, - OptimizeConstants.EventDataKeys.PROPOSITION_INTERACTIONS); - if (OptimizeUtils.isNullOrEmpty(propositionInteractionsXdm)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleTrackPropositions - Cannot process the track propositions request" - + " event, provided proposition interactions map is null or empty."); - return; - } - - final Map edgeEventData = new HashMap<>(); - edgeEventData.put(OptimizeConstants.JsonKeys.XDM, propositionInteractionsXdm); - - // Add override datasetId - if (configData.containsKey( - OptimizeConstants.Configuration.OPTIMIZE_OVERRIDE_DATASET_ID)) { - final String overrideDatasetId = - DataReader.getString( - configData, - OptimizeConstants.Configuration.OPTIMIZE_OVERRIDE_DATASET_ID); - if (!OptimizeUtils.isNullOrEmpty(overrideDatasetId)) { - edgeEventData.put(OptimizeConstants.JsonKeys.DATASET_ID, overrideDatasetId); - } - } - - final Event edgeEvent = - new Event.Builder( - OptimizeConstants.EventNames - .EDGE_PROPOSITION_INTERACTION_REQUEST, - OptimizeConstants.EventType.EDGE, - OptimizeConstants.EventSource.REQUEST_CONTENT) - .setEventData(edgeEventData) - .build(); - - getApi().dispatch(edgeEvent); - - } catch (final Exception e) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleTrackPropositions - Failed to process track propositions request event" - + " due to an exception (%s)!", - e.getLocalizedMessage()); - } - } - - /** - * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#REQUEST_RESET}. - * - *

This method clears previously cached propositions in the SDK. - * - * @param event incoming {@link Event} object to be processed. - */ - void handleClearPropositions(@NonNull final Event event) { - cachedPropositions.clear(); - previewCachedPropositions.clear(); - } - - /** - * Handles the event with type {@value EventType#SYSTEM} and source {@value - * OptimizeConstants.EventSource#DEBUG}. - * - *

A debug event allows the optimize extension to processes non-production workflows. - * - * @param event the debug {@link Event} to be handled. - */ - void handleDebugEvent(@NonNull final Event event) { - try { - if (OptimizeUtils.isNullOrEmpty(event.getEventData())) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleDebugEvent - Ignoring the Optimize Debug event, either event is null" - + " or event data is null/ empty."); - return; - } - - if (!OptimizeUtils.isPersonalizationDebugEvent(event)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleDebugEvent - Ignoring Optimize Debug event, either handle type is" - + " not com.adobe.eventType.system or source is not" - + " com.adobe.eventSource.debug"); - return; - } - - final Map eventData = event.getEventData(); - - final List> payload = - DataReader.getTypedListOfMap( - Object.class, eventData, OptimizeConstants.Edge.PAYLOAD); - if (OptimizeUtils.isNullOrEmpty(payload)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleDebugEvent - Cannot process the Debug event, propositions list is" - + " either null or empty in the response."); - return; - } - - final Map propositionsMap = new HashMap<>(); - for (final Map propositionData : payload) { - final OptimizeProposition optimizeProposition = - OptimizeProposition.fromEventData(propositionData); - if (optimizeProposition != null - && !OptimizeUtils.isNullOrEmpty(optimizeProposition.getOffers())) { - final DecisionScope scope = new DecisionScope(optimizeProposition.getScope()); - propositionsMap.put(scope, optimizeProposition); - } - } - - if (OptimizeUtils.isNullOrEmpty(propositionsMap)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleDebugEvent - Cannot process the Debug event, no propositions with" - + " valid offers are present in the response."); - return; - } - - previewCachedPropositions.putAll(propositionsMap); - - final List> propositionsList = new ArrayList<>(); - for (final OptimizeProposition optimizeProposition : propositionsMap.values()) { - propositionsList.add(optimizeProposition.toEventData()); - } - final Map notificationData = new HashMap<>(); - notificationData.put(OptimizeConstants.EventDataKeys.PROPOSITIONS, propositionsList); - - final Event notificationEvent = - new Event.Builder( - OptimizeConstants.EventNames.OPTIMIZE_NOTIFICATION, - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.NOTIFICATION) - .setEventData(notificationData) - .build(); - - // Dispatch notification event - getApi().dispatch(notificationEvent); - } catch (final Exception e) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "handleDebugEvent - Cannot process the Debug event due to an exception (%s)!", - e.getLocalizedMessage()); - } - } - - /** - * Retrieves the {@code Configuration} shared state versioned at the current {@code event}. - * - * @param event incoming {@link Event} instance. - * @return {@code Map} containing configuration data. - */ - private Map retrieveConfigurationSharedState(final Event event) { - SharedStateResult configurationSharedState = - getApi().getSharedState( - OptimizeConstants.Configuration.EXTENSION_NAME, - event, - false, - SharedStateResolution.ANY); - return configurationSharedState != null ? configurationSharedState.getValue() : null; - } - - /** - * Retrieves the {@code List} containing valid scopes. - * - *

This method returns null if the given {@code decisionScopesData} list is null, or empty, - * or if there is no valid decision scope in the provided list. - * - * @param decisionScopesData input {@code List>} containing scope data. - * @return {@code List} containing valid scopes. - * @see DecisionScope#isValid() - */ - private List retrieveValidDecisionScopes( - final List> decisionScopesData) { - if (OptimizeUtils.isNullOrEmpty(decisionScopesData)) { - Log.debug( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "retrieveValidDecisionScopes - No valid decision scopes are retrieved, provided" - + " decision scopes list is null or empty."); - return null; - } - - final List validScopes = new ArrayList<>(); - for (final Map scopeData : decisionScopesData) { - final DecisionScope scope = DecisionScope.fromEventData(scopeData); - if (scope == null || !scope.isValid()) { - continue; - } - validScopes.add(scope); - } - - if (validScopes.size() == 0) { - Log.warning( - OptimizeConstants.LOG_TAG, - SELF_TAG, - "retrieveValidDecisionScopes - No valid decision scopes are retrieved, provided" - + " list of decision scopes has no valid scope."); - return null; - } - - return validScopes; - } - - /** - * Creates {@value OptimizeConstants.EventType#OPTIMIZE}, {@value - * OptimizeConstants.EventSource#RESPONSE_CONTENT} event with the given {@code error} in event - * data. - * - * @return {@link Event} instance. - */ - private Event createResponseEventWithError(final Event event, final AdobeError error) { - final Map eventData = new HashMap<>(); - eventData.put(OptimizeConstants.EventDataKeys.RESPONSE_ERROR, error.getErrorCode()); - - return new Event.Builder( - OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.RESPONSE_CONTENT) - .setEventData(eventData) - .inResponseToEvent(event) - .build(); - } - - private Event createResponseEventWithError(final Event event, final AEPOptimizeError error) { - final Map eventData = new HashMap<>(); - eventData.put(OptimizeConstants.EventDataKeys.RESPONSE_ERROR, error); - - return new Event.Builder( - OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, - OptimizeConstants.EventType.OPTIMIZE, - OptimizeConstants.EventSource.RESPONSE_CONTENT) - .setEventData(eventData) - .inResponseToEvent(event) - .build(); - } - - @VisibleForTesting - Map getCachedPropositions() { - return cachedPropositions; - } - - @VisibleForTesting - void setCachedPropositions(final Map cachedPropositions) { - this.cachedPropositions = cachedPropositions; - } - - @VisibleForTesting - Map getPreviewCachedPropositions() { - return previewCachedPropositions; - } - - @VisibleForTesting - void setPreviewCachedPropositions( - final Map previewCachedPropositions) { - this.previewCachedPropositions = previewCachedPropositions; - } - - @VisibleForTesting - Map getPropositionsInProgress() { - return propositionsInProgress; - } - - @VisibleForTesting - void setPropositionsInProgress( - final Map propositionsInProgress) { - this.propositionsInProgress.clear(); - this.propositionsInProgress.putAll(propositionsInProgress); - } - - @VisibleForTesting - Map> getUpdateRequestEventIdsInProgress() { - return updateRequestEventIdsInProgress; - } - - @VisibleForTesting - void setUpdateRequestEventIdsInProgress( - final String eventId, final List expectedScopes) { - updateRequestEventIdsInProgress.put(eventId, expectedScopes); - } - - @VisibleForTesting - void setEventsDispatcher(final SerialWorkDispatcher eventsDispatcher) { - this.eventsDispatcher = eventsDispatcher; + public Class getExtensionV2Class() { + return OptimizeExtensionV2.class; } -} +} \ No newline at end of file diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionV2.kt b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionV2.kt new file mode 100644 index 00000000..5bbd5bca --- /dev/null +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionV2.kt @@ -0,0 +1,1072 @@ +package com.adobe.marketing.mobile.optimize + +import com.adobe.marketing.mobile.AdobeCallbackWithError +import com.adobe.marketing.mobile.AdobeError +import com.adobe.marketing.mobile.Event +import com.adobe.marketing.mobile.EventType +import com.adobe.marketing.mobile.ExtensionApiV2 +import com.adobe.marketing.mobile.ExtensionApiV2Builder +import com.adobe.marketing.mobile.ExtensionV2 +import com.adobe.marketing.mobile.ExtensionV2Delegate +import com.adobe.marketing.mobile.MobileCore +import com.adobe.marketing.mobile.SharedStateResolution +import com.adobe.marketing.mobile.SharedStateStatus +import com.adobe.marketing.mobile.optimize.AEPOptimizeError.Companion.getUnexpectedError +import com.adobe.marketing.mobile.optimize.ConfigUtils.retrieveOptimizeRequestTimeout +import com.adobe.marketing.mobile.services.Log +import com.adobe.marketing.mobile.util.DataReader +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull +import java.util.Arrays +import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +private typealias PropositionReceiver = (Map) -> Unit + +private object PropositionManager { + private val mutex = Mutex() + private val map = mutableMapOf() + private val receivers = ConcurrentHashMap>() + + suspend fun updatePending(keys: List) = + mutex.withLock { map.putAll(keys.map { it to null }) } + + suspend fun clearAll() = mutex.withLock { + map.clear() + receivers.clear() + } + + suspend fun update(updates: Map) = mutex.withLock { + map.putAll(updates) + notifyReceiverIfReady() + } + + private fun notifyReceiverIfReady() = receivers.forEach { (receiver, keys) -> + if (keys.all { key -> map.containsKey(key) && map[key] != null }) { + receiver(keys.associateWith { key -> map[key]!! }) + receivers.remove(receiver) + } + } + + suspend fun get(keys: List): Map = + mutex.withLock { + return if (keys.all { key -> map.containsKey(key) && map[key] != null }) { + keys.associateWith { key -> map[key]!! } + } else { + suspendCancellableCoroutine { + register(keys) { map -> it.resume(map) } + } + } + } + + suspend fun getAllOrNull(keys: List): Map? = + mutex.withLock { + return if (keys.all { key -> map.containsKey(key) && map[key] != null }) { + keys.associateWith { key -> map[key]!! } + } else { + return null + } + } + + + private fun register(keys: List, receiver: PropositionReceiver) { + receivers[receiver] = keys + } +} + +class OptimizeExtensionDelegate : ExtensionV2Delegate() { + override fun getExtensionV2Class(): Class { + return OptimizeExtensionV2::class.java + } +} + +class OptimizeExtensionV2 : ExtensionV2() { + override val metadata: Map? + get() = null + + override val name: String + get() = OptimizeConstants.EXTENSION_NAME + + override val version: String + get() = OptimizeConstants.EXTENSION_VERSION + + override val friendlyName: String + get() = OptimizeConstants.FRIENDLY_NAME + + private val updateDispatcher = Dispatchers.IO.limitedParallelism(1) + private val scope = CoroutineScope(updateDispatcher) + + companion object { + private const val SELF_TAG = "OptimizeExtension" + + // List containing the schema strings for the proposition items supported by the SDK, sent in + // the personalization query request. + val supportedSchemas: List = Arrays.asList( // Target schemas + OptimizeConstants.JsonValues.SCHEMA_TARGET_HTML, + OptimizeConstants.JsonValues.SCHEMA_TARGET_JSON, + OptimizeConstants.JsonValues.SCHEMA_TARGET_DEFAULT, // Offer Decisioning schemas + + OptimizeConstants.JsonValues.SCHEMA_OFFER_HTML, + OptimizeConstants.JsonValues.SCHEMA_OFFER_JSON, + OptimizeConstants.JsonValues.SCHEMA_OFFER_IMAGE, + OptimizeConstants.JsonValues.SCHEMA_OFFER_TEXT + ) + + // List containing recoverable network error codes being retried by Edge Network Service + private val recoverableNetworkErrorCodes: List = Arrays.asList( + OptimizeConstants.HTTPResponseCodes.clientTimeout, + OptimizeConstants.HTTPResponseCodes.tooManyRequests, + OptimizeConstants.HTTPResponseCodes.badGateway, + OptimizeConstants.HTTPResponseCodes.serviceUnavailable, + OptimizeConstants.HTTPResponseCodes.gatewayTimeout + ) + + private val updateRequestEventIdsErrors: MutableMap = + ConcurrentHashMap() + } + + private var previewCachedPropositions: MutableMap = + ConcurrentHashMap() + + private var api: ExtensionApiV2? = null + + override fun onRegistered(buildExtensionApi: ExtensionApiV2Builder) { + api = buildExtensionApi { event -> + if (OptimizeConstants.EventType.OPTIMIZE.equals(event.type, ignoreCase = true) + && OptimizeConstants.EventSource.REQUEST_CONTENT.equals( + event.source, ignoreCase = true + ) + ) { + val configurationSharedState = + api?.getSharedState( + OptimizeConstants.Configuration.EXTENSION_NAME, + event, + false, + SharedStateResolution.ANY + ) + return@buildExtensionApi configurationSharedState != null + && configurationSharedState.status == SharedStateStatus.SET + } + return@buildExtensionApi true + } + api?.registerEventListener( + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.REQUEST_CONTENT + ) { event: Event -> + this.handleOptimizeRequestContent( + event + ) + } + + api?.registerEventListener( + OptimizeConstants.EventType.EDGE, + OptimizeConstants.EventSource.EDGE_PERSONALIZATION_DECISIONS + ) { event: Event -> this.handleEdgeResponse(event) } + + api?.registerEventListener( + OptimizeConstants.EventType.EDGE, + OptimizeConstants.EventSource.ERROR_RESPONSE_CONTENT + ) { event: Event -> + this.handleEdgeErrorResponse( + event + ) + } + + api?.registerEventListener( + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.REQUEST_RESET + ) { event: Event -> + this.handleClearPropositions( + event + ) + } + + // Register listener - Mobile Core `resetIdentities()` API dispatches generic identity + // request reset event. + api?.registerEventListener( + OptimizeConstants.EventType.GENERIC_IDENTITY, + OptimizeConstants.EventSource.REQUEST_RESET + ) { event: Event -> + this.handleClearPropositions( + event + ) + } + + api?.registerEventListener( + EventType.SYSTEM, + OptimizeConstants.EventSource.DEBUG + ) { event: Event -> this.handleDebugEvent(event) } + + } + + /** + * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value + * * OptimizeConstants.EventSource#REQUEST_CONTENT}. + * + * + * This method handles the event based on the value of {@value + * * OptimizeConstants.EventDataKeys#REQUEST_TYPE} in the event data of current `event` + * + * @param event incoming [Event] object to be processed. + */ + private suspend fun handleOptimizeRequestContent(event: Event) { + if (OptimizeUtils.isNullOrEmpty(event.eventData)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleOptimizeRequestContent - Ignoring the Optimize request event, either" + + " event is null or event data is null/ empty." + ) + return + } + + val eventData = event.eventData + val requestType = + DataReader.optString(eventData, OptimizeConstants.EventDataKeys.REQUEST_TYPE, "") + + when (requestType) { + OptimizeConstants.EventDataValues.REQUEST_TYPE_UPDATE -> handleUpdatePropositions(event) + OptimizeConstants.EventDataValues.REQUEST_TYPE_GET -> try { + // Fetch decision scopes from the event + val decisionScopesData = + DataReader.getTypedListOfMap( + Any::class.java, + eventData, + OptimizeConstants.EventDataKeys.DECISION_SCOPES + ) + val eventDecisionScopes = + retrieveValidDecisionScopes(decisionScopesData) + + if (OptimizeUtils.isNullOrEmpty(eventDecisionScopes)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + ("handleOptimizeRequestContent - Cannot process the get propositions" + + " request event, provided list of decision scopes has no" + + " valid scope.") + ) + api?.dispatch( + createResponseEventWithError( + event, AdobeError.UNEXPECTED_ERROR + ) + ) + return + } + val validScopes = retrieveValidDecisionScopes(decisionScopesData) + if (OptimizeUtils.isNullOrEmpty(validScopes)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleGetPropositions - Cannot process the get propositions request event," + + " provided list of decision scopes has no valid scope." + ) + api?.dispatch(createResponseEventWithError(event, AdobeError.UNEXPECTED_ERROR)) + return + } + scope.launch { + val fetchedPropositions = withTimeoutOrNull(5000) { + PropositionManager.get(validScopes!!) + } + if (fetchedPropositions != null) { + Log.trace( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleOptimizeRequestContent - All scopes are cached and none are" + + " in progress, dispatching event directly." + ) + + try { + + val propositionsList: MutableList> = ArrayList() + fetchedPropositions.forEach { (_, proposition) -> + propositionsList.add(proposition.toEventData()) + } + val previewPropositionsList: MutableList> = ArrayList() + for (scope in validScopes!!) { + if (previewCachedPropositions.containsKey(scope)) { + val optimizeProposition = + previewCachedPropositions[scope] + previewPropositionsList.add(optimizeProposition!!.toEventData()) + } + } + + val responseEventData: MutableMap = HashMap() + + if (!previewPropositionsList.isEmpty()) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "Preview Mode is enabled." + ) + responseEventData[OptimizeConstants.EventDataKeys.PROPOSITIONS] = + previewPropositionsList + } else { + responseEventData[OptimizeConstants.EventDataKeys.PROPOSITIONS] = + propositionsList + } + + val responseEvent = + Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.RESPONSE_CONTENT + ) + .setEventData(responseEventData) + .inResponseToEvent(event) + .build() + + api?.dispatch(responseEvent) + } catch (e: Exception) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleGetPropositions - Failed to process get propositions request event due" + + " to an exception (%s)!", + e.localizedMessage + ) + api?.dispatch( + createResponseEventWithError( + event, + AdobeError.UNEXPECTED_ERROR + ) + ) + } + } else { + api?.dispatch( + createResponseEventWithError( + event, + AdobeError.UNEXPECTED_ERROR + ) + ) + } + } + } catch (e: Exception) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleOptimizeRequestContent - Failed to process get propositions" + + " request event due to an exception (%s)!", + e.localizedMessage + ) + } + + OptimizeConstants.EventDataValues.REQUEST_TYPE_TRACK -> handleTrackPropositions(event) + else -> Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleOptimizeRequestContent - Ignoring the Optimize request event," + + " provided request type (%s) is not handled by this extension.", + requestType + ) + } + } + + /** + * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value + * * OptimizeConstants.EventSource#REQUEST_CONTENT}. + * + * + * This method dispatches an event to the Edge network extension to send personalization + * query request to the Experience Edge network. The dispatched event contains additional XDM + * and/ or free-form data, read from the incoming event, to be attached to the Edge request. + * + * @param event incoming [Event] object to be processed. + */ + private suspend fun handleUpdatePropositions(event: Event) { + val eventData = event.eventData + + val configData = retrieveConfigurationSharedState(event) + if (OptimizeUtils.isNullOrEmpty(configData)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleUpdatePropositions - Cannot process the update propositions request" + + " event, Configuration shared state is not available." + ) + return + } + + val decisionScopesData = + DataReader.getTypedListOfMap( + Any::class.java, + eventData, + OptimizeConstants.EventDataKeys.DECISION_SCOPES + ) + val validScopes = retrieveValidDecisionScopes(decisionScopesData) + if (OptimizeUtils.isNullOrEmpty(validScopes)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleUpdatePropositions - Cannot process the update propositions request" + + " event, provided list of decision scopes has no valid scope." + ) + return + } + + val edgeEventData: MutableMap = HashMap() + + // Add query + val queryPersonalization: MutableMap = HashMap() + queryPersonalization[OptimizeConstants.JsonKeys.SCHEMAS] = supportedSchemas + + val validScopeNames: MutableList = ArrayList() + for (scope in validScopes!!) { + validScopeNames.add(scope!!.name) + } + queryPersonalization[OptimizeConstants.JsonKeys.DECISION_SCOPES] = validScopeNames + + val query: MutableMap = HashMap() + query[OptimizeConstants.JsonKeys.QUERY_PERSONALIZATION] = queryPersonalization + edgeEventData[OptimizeConstants.JsonKeys.QUERY] = query + + // Add xdm + val xdm: MutableMap = HashMap() + if (eventData.containsKey(OptimizeConstants.EventDataKeys.XDM)) { + val inputXdm = + DataReader.getTypedMap( + Any::class.java, eventData, OptimizeConstants.EventDataKeys.XDM + ) + if (!OptimizeUtils.isNullOrEmpty(inputXdm)) { + xdm.putAll(inputXdm) + } + } + xdm[OptimizeConstants.JsonKeys.EXPERIENCE_EVENT_TYPE] = + OptimizeConstants.JsonValues.EE_EVENT_TYPE_PERSONALIZATION + edgeEventData[OptimizeConstants.JsonKeys.XDM] = xdm + + // Add data + val data: MutableMap = HashMap() + if (eventData.containsKey(OptimizeConstants.EventDataKeys.DATA)) { + val inputData = + DataReader.getTypedMap( + Any::class.java, eventData, OptimizeConstants.EventDataKeys.DATA + ) + if (!OptimizeUtils.isNullOrEmpty(inputData)) { + data.putAll(inputData) + edgeEventData[OptimizeConstants.JsonKeys.DATA] = data + } + } + + // Add the flag to request sendCompletion + val request: MutableMap = HashMap() + request[OptimizeConstants.JsonKeys.REQUEST_SEND_COMPLETION] = true + edgeEventData[OptimizeConstants.JsonKeys.REQUEST] = request + + // Add override datasetId + if (configData.containsKey( + OptimizeConstants.Configuration.OPTIMIZE_OVERRIDE_DATASET_ID + ) + ) { + val overrideDatasetId = + DataReader.getString( + configData, + OptimizeConstants.Configuration.OPTIMIZE_OVERRIDE_DATASET_ID + ) + if (!OptimizeUtils.isNullOrEmpty(overrideDatasetId)) { + edgeEventData[OptimizeConstants.JsonKeys.DATASET_ID] = overrideDatasetId + } + } + + val edgeEvent = + Event.Builder( + OptimizeConstants.EventNames.EDGE_PERSONALIZATION_REQUEST, + OptimizeConstants.EventType.EDGE, + OptimizeConstants.EventSource.REQUEST_CONTENT + ) + .setEventData(edgeEventData) + .chainToParentEvent(event) + .build() + + val timeoutMillis = retrieveOptimizeRequestTimeout(event, configData) + + PropositionManager.updatePending(validScopes) + + val edgeResponseEvent: Event? = suspendCoroutine { + MobileCore.dispatchEventWithResponseCallback( + edgeEvent, + timeoutMillis, + object : AdobeCallbackWithError { + override fun call(callbackEvent: Event?) { + it.resume(callbackEvent) + } + + override fun fail(error: AdobeError) { + it.resume(null) + } + }) + } + + if (edgeResponseEvent == null || OptimizeUtils.isNullOrEmpty( + OptimizeUtils.getRequestEventId( + edgeResponseEvent + ) + ) + ) { + api?.dispatch(createResponseEventWithError(event, getUnexpectedError())) + return + } + + + val responseEventData: MutableMap = java.util.HashMap() + val aepOptimizeError = + updateRequestEventIdsErrors[OptimizeUtils.getRequestEventId(edgeResponseEvent)] + if (aepOptimizeError != null) { + responseEventData[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] = + aepOptimizeError.toEventData() + } + + val propositionsList: MutableList> = java.util.ArrayList() + + validScopes.forEach { scope -> + val map = PropositionManager.getAllOrNull(listOf(scope)) + if (map != null) { + propositionsList.add(map[scope]!!.toEventData()) + } + } + + responseEventData[OptimizeConstants.EventDataKeys.PROPOSITIONS] = propositionsList + + val responseEvent = + Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.RESPONSE_CONTENT + ) + .setEventData(responseEventData) + .inResponseToEvent(event) + .build() + + api?.dispatch(responseEvent) + } + + /** + * Handles the event with type {@value OptimizeConstants.EventType#EDGE} and source {@value + * * OptimizeConstants.EventSource#EDGE_PERSONALIZATION_DECISIONS}. + * + * + * This method caches the propositions, returned in the Edge response, in the SDK. It also + * dispatches a personalization notification event with the received propositions. + * + * @param event incoming [Event] object to be processed. + */ + private suspend fun handleEdgeResponse(event: Event) { + try { + val eventData = event.eventData + val requestEventId = OptimizeUtils.getRequestEventId(event) + + if (!OptimizeUtils.isPersonalizationDecisionsResponse(event) || OptimizeUtils.isNullOrEmpty( + requestEventId + ) + ) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + ("handleEdgeResponse - Ignoring Edge event, either handle type is not" + + " personalization:decisions, or the response isn't intended for this" + + " extension.") + ) + return + } + + val payload = + DataReader.getTypedListOfMap( + Any::class.java, eventData, OptimizeConstants.Edge.PAYLOAD + ) + if (OptimizeUtils.isNullOrEmpty(payload)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + ("handleEdgeResponse - Cannot process the Edge personalization:decisions" + + " event, propositions list is either null or empty in the Edge" + + " response.") + ) + return + } + + val propositionsMap: MutableMap = HashMap() + for (propositionData in payload) { + val optimizeProposition = + OptimizeProposition.fromEventData(propositionData) + if (optimizeProposition != null + && !OptimizeUtils.isNullOrEmpty(optimizeProposition.offers) + ) { + val scope = DecisionScope(optimizeProposition.scope) + propositionsMap[scope] = optimizeProposition + } + } + + if (OptimizeUtils.isNullOrEmpty(propositionsMap)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + ("handleEdgeResponse - Cannot process the Edge personalization:decisions" + + " event, no propositions with valid offers are present in the Edge" + + " response.") + ) + return + } + + PropositionManager.update(propositionsMap) + + val propositionsList: MutableList> = ArrayList() + for (optimizeProposition in propositionsMap.values) { + propositionsList.add(optimizeProposition!!.toEventData()) + } + val notificationData: MutableMap = HashMap() + notificationData[OptimizeConstants.EventDataKeys.PROPOSITIONS] = propositionsList + + val edgeEvent = + Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_NOTIFICATION, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.NOTIFICATION + ) + .setEventData(notificationData) + .build() + + // Dispatch notification event + api?.dispatch(edgeEvent) + } catch (e: Exception) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleEdgeResponse - Cannot process the Edge personalization:decisions event" + + " due to an exception (%s)!", + e.localizedMessage + ) + } + } + + /** + * Handles the event with type {@value OptimizeConstants.EventType#EDGE} and source {@value + * * OptimizeConstants.EventSource#ERROR_RESPONSE_CONTENT}. + * + * + * This method logs the error information, returned in Edge response, specifying error type + * along with a detail message. + * + * @param event incoming [Event] object to be processed. + */ + private fun handleEdgeErrorResponse(event: Event) { + try { + val eventData = event.eventData + val requestEventId = OptimizeUtils.getRequestEventId(event) + + if (!OptimizeUtils.isEdgeErrorResponseContent(event) || OptimizeUtils.isNullOrEmpty( + requestEventId + ) + ) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + ("handleEdgeResponse - Ignoring Edge event, either handle type is not edge" + + " error response content, or the response isn't intended for this" + + " extension.") + ) + return + } + + if (OptimizeUtils.isNullOrEmpty(event.eventData)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleEdgeErrorResponse - Ignoring the Edge error response event, either" + + " event is null or event data is null/ empty." + ) + return + } + + val errorType = + DataReader.optString( + eventData, + OptimizeConstants.Edge.ErrorKeys.TYPE, + OptimizeConstants.ERROR_UNKNOWN + ) + val errorStatus = + DataReader.optInt( + eventData, + OptimizeConstants.Edge.ErrorKeys.STATUS, + OptimizeConstants.UNKNOWN_STATUS + ) + val errorTitle = + DataReader.optString( + eventData, + OptimizeConstants.Edge.ErrorKeys.TITLE, + OptimizeConstants.ERROR_UNKNOWN + ) + val errorDetail = + DataReader.optString( + eventData, + OptimizeConstants.Edge.ErrorKeys.DETAIL, + OptimizeConstants.ERROR_UNKNOWN + ) + val errorReport = + DataReader.optTypedMap( + Any::class.java, + eventData, + OptimizeConstants.Edge.ErrorKeys.REPORT, + HashMap() + ) + + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + (""" + handleEdgeErrorResponse - Decisioning Service error! Error type: (%s), + title: (%s), + detail: (%s), + status: (%s), + report: (%s) + """.trimIndent()), + errorType, + errorTitle, + errorDetail, + errorStatus, + errorReport + ) + + // Check if the errorStatus is in the list of recoverable error codes + if (recoverableNetworkErrorCodes.contains(errorStatus)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "Recoverable error encountered: Status %d", + errorStatus + ) + return + } else { + val aepOptimizeError = + AEPOptimizeError( + errorType, errorStatus, errorTitle, errorDetail, errorReport, null + ) + updateRequestEventIdsErrors[requestEventId] = + aepOptimizeError + } + } catch (e: Exception) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleEdgeResponse - Cannot process the Edge Error Response event" + + " due to an exception (%s)!", + e.localizedMessage + ) + } + } + + /** + * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value + * * OptimizeConstants.EventSource#REQUEST_CONTENT}. + * + * + * This method caches the propositions, returned in the Edge response, in the SDK. It also + * dispatches an optimize response event with the propositions for the requested decision + * scopes. + * + * @param event incoming [Event] object to be processed. + */ + private fun handleGetPropositions(event: Event) { + + } + + /** + * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value + * * OptimizeConstants.EventSource#REQUEST_CONTENT}. + * + * + * This method dispatches an event to the Edge network extension to send proposition + * interactions information to the Experience Edge network. The dispatched event may contain an + * override `datasetId` indicating the dataset which will be used for storing the + * Experience Events sent to the Edge network. + * + * @param event incoming [Event] object to be processed. + */ + private suspend fun handleTrackPropositions(event: Event) { + val eventData = event.eventData + + val configData = retrieveConfigurationSharedState(event) + if (OptimizeUtils.isNullOrEmpty(configData)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleTrackPropositions - Cannot process the track propositions request event," + + " Configuration shared state is not available." + ) + return + } + + try { + val propositionInteractionsXdm = + DataReader.getTypedMap( + Any::class.java, + eventData, + OptimizeConstants.EventDataKeys.PROPOSITION_INTERACTIONS + ) + if (OptimizeUtils.isNullOrEmpty(propositionInteractionsXdm)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleTrackPropositions - Cannot process the track propositions request" + + " event, provided proposition interactions map is null or empty." + ) + return + } + + val edgeEventData: MutableMap = HashMap() + edgeEventData[OptimizeConstants.JsonKeys.XDM] = propositionInteractionsXdm + + // Add override datasetId + if (configData!!.containsKey( + OptimizeConstants.Configuration.OPTIMIZE_OVERRIDE_DATASET_ID + ) + ) { + val overrideDatasetId = + DataReader.getString( + configData, + OptimizeConstants.Configuration.OPTIMIZE_OVERRIDE_DATASET_ID + ) + if (!OptimizeUtils.isNullOrEmpty(overrideDatasetId)) { + edgeEventData[OptimizeConstants.JsonKeys.DATASET_ID] = overrideDatasetId + } + } + + val edgeEvent = + Event.Builder( + OptimizeConstants.EventNames + .EDGE_PROPOSITION_INTERACTION_REQUEST, + OptimizeConstants.EventType.EDGE, + OptimizeConstants.EventSource.REQUEST_CONTENT + ) + .setEventData(edgeEventData) + .build() + + api?.dispatch(edgeEvent) + } catch (e: Exception) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleTrackPropositions - Failed to process track propositions request event" + + " due to an exception (%s)!", + e.localizedMessage + ) + } + } + + /** + * Handles the event with type {@value OptimizeConstants.EventType#OPTIMIZE} and source {@value + * * OptimizeConstants.EventSource#REQUEST_RESET}. + * + * + * This method clears previously cached propositions in the SDK. + * + * @param event incoming [Event] object to be processed. + */ + private suspend fun handleClearPropositions(event: Event) { + PropositionManager.clearAll() + previewCachedPropositions.clear() + } + + /** + * Handles the event with type {@value EventType#SYSTEM} and source {@value + * * OptimizeConstants.EventSource#DEBUG}. + * + * + * A debug event allows the optimize extension to processes non-production workflows. + * + * @param event the debug [Event] to be handled. + */ + private fun handleDebugEvent(event: Event) { + try { + if (OptimizeUtils.isNullOrEmpty(event.eventData)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Ignoring the Optimize Debug event, either event is null" + + " or event data is null/ empty." + ) + return + } + + if (!OptimizeUtils.isPersonalizationDebugEvent(event)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + ("handleDebugEvent - Ignoring Optimize Debug event, either handle type is" + + " not com.adobe.eventType.system or source is not" + + " com.adobe.eventSource.debug") + ) + return + } + + val eventData = event.eventData + + val payload = + DataReader.getTypedListOfMap( + Any::class.java, eventData, OptimizeConstants.Edge.PAYLOAD + ) + if (OptimizeUtils.isNullOrEmpty(payload)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Cannot process the Debug event, propositions list is" + + " either null or empty in the response." + ) + return + } + + val propositionsMap: MutableMap = HashMap() + for (propositionData in payload) { + val optimizeProposition = + OptimizeProposition.fromEventData(propositionData) + if (optimizeProposition != null + && !OptimizeUtils.isNullOrEmpty(optimizeProposition.offers) + ) { + val scope = DecisionScope(optimizeProposition.scope) + propositionsMap[scope] = optimizeProposition + } + } + + if (OptimizeUtils.isNullOrEmpty(propositionsMap)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Cannot process the Debug event, no propositions with" + + " valid offers are present in the response." + ) + return + } + + previewCachedPropositions.putAll(propositionsMap) + + val propositionsList: MutableList> = ArrayList() + for (optimizeProposition in propositionsMap.values) { + propositionsList.add(optimizeProposition!!.toEventData()) + } + val notificationData: MutableMap = HashMap() + notificationData[OptimizeConstants.EventDataKeys.PROPOSITIONS] = propositionsList + + val notificationEvent = + Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_NOTIFICATION, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.NOTIFICATION + ) + .setEventData(notificationData) + .build() + + // Dispatch notification event + api?.dispatch(notificationEvent) + } catch (e: Exception) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Cannot process the Debug event due to an exception (%s)!", + e.localizedMessage + ) + } + } + + /** + * Retrieves the `Configuration` shared state versioned at the current `event`. + * + * @param event incoming [Event] instance. + * @return `Map` containing configuration data. + */ + suspend fun retrieveConfigurationSharedState(event: Event): Map { + val configurationSharedState = + api?.getSharedState( + OptimizeConstants.Configuration.EXTENSION_NAME, + event, + false, + SharedStateResolution.ANY + ) + return configurationSharedState?.value ?: emptyMap() + } + + /** + * Retrieves the `List` containing valid scopes. + * + * + * This method returns null if the given `decisionScopesData` list is null, or empty, + * or if there is no valid decision scope in the provided list. + * + * @param decisionScopesData input `List>` containing scope data. + * @return `List` containing valid scopes. + * @see DecisionScope.isValid + */ + private fun retrieveValidDecisionScopes( + decisionScopesData: List?> + ): List? { + if (OptimizeUtils.isNullOrEmpty(decisionScopesData)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "retrieveValidDecisionScopes - No valid decision scopes are retrieved, provided" + + " decision scopes list is null or empty." + ) + return null + } + + val validScopes: MutableList = ArrayList() + for (scopeData in decisionScopesData) { + val scope = DecisionScope.fromEventData(scopeData) + if (scope == null || !scope.isValid) { + continue + } + validScopes.add(scope) + } + + if (validScopes.size == 0) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "retrieveValidDecisionScopes - No valid decision scopes are retrieved, provided" + + " list of decision scopes has no valid scope." + ) + return null + } + + return validScopes + } + + /** + * Creates {@value OptimizeConstants.EventType#OPTIMIZE}, {@value + * * OptimizeConstants.EventSource#RESPONSE_CONTENT} event with the given `error` in event + * data. + * + * @return [Event] instance. + */ + private fun createResponseEventWithError(event: Event, error: AdobeError): Event { + val eventData: MutableMap = HashMap() + eventData[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] = error.errorCode + + return Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.RESPONSE_CONTENT + ) + .setEventData(eventData) + .inResponseToEvent(event) + .build() + } + + private fun createResponseEventWithError(event: Event, error: AEPOptimizeError): Event { + val eventData: MutableMap = HashMap() + eventData[OptimizeConstants.EventDataKeys.RESPONSE_ERROR] = error + + return Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_RESPONSE, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.RESPONSE_CONTENT + ) + .setEventData(eventData) + .inResponseToEvent(event) + .build() + } + +} \ No newline at end of file