\d+)'
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index a48429109..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,83 +0,0 @@
-language: java
-dist: trusty
-jdk:
- - openjdk8
- - oraclejdk8
- - oraclejdk9
-install: true
-env:
- - optimizely_default_parser=GSON_CONFIG_PARSER
- - optimizely_default_parser=JACKSON_CONFIG_PARSER
- - optimizely_default_parser=JSON_CONFIG_PARSER
- - optimizely_default_parser=JSON_SIMPLE_CONFIG_PARSER
-script:
- - "./gradlew clean"
- - "./gradlew exhaustiveTest"
- - "if [[ -n $TRAVIS_TAG ]]; then
- ./gradlew ship;
- else
- ./gradlew build;
- fi"
-cache:
- gradle: true
- directories:
- - "$HOME/.gradle/caches"
- - "$HOME/.gradle/wrapper"
-branches:
- only:
- - master
- - /^\d+\.\d+\.(\d|[x])+(-SNAPSHOT|-alpha|-beta)?\d*$/ # trigger builds on tags which are semantically versioned to ship the SDK.
-after_success:
- - ./gradlew coveralls uploadArchives --console plain
-after_failure:
- - cat /home/travis/build/optimizely/java-sdk/core-api/build/reports/findbugs/main.html
- - cat /home/travis/build/optimizely/java-sdk/core-api/build/reports/findbugs/test.html
-
-# Integration tests need to run first to reset the PR build status to pending
-stages:
- - 'Lint markdown files'
- - 'Integration tests'
- - 'Benchmarking tests'
- - 'Test'
-
-jobs:
- include:
- - stage: 'Lint markdown files'
- os: linux
- language: generic
- install: gem install awesome_bot
- script:
- - find . -type f -name '*.md' -exec awesome_bot {} \;
- notifications:
- email: false
-
- - stage: 'Lint markdown files'
- os: linux
- language: generic
- before_install: skip
- install:
- - npm i -g markdown-spellcheck
- before_script:
- - wget --quiet https://raw.githubusercontent.com/optimizely/mdspell-config/master/.spelling
- script:
- - mdspell -a -n -r --en-us '**/*.md'
- after_success: skip
-
- - &integrationtest
- stage: 'Integration tests'
- addons:
- srcclr: true
- merge_mode: replace
- env: SDK=java SDK_BRANCH=$TRAVIS_PULL_REQUEST_BRANCH
- cache: false
- language: minimal
- before_install: skip
- install: skip
- before_script:
- - mkdir $HOME/travisci-tools && pushd $HOME/travisci-tools && git init && git pull https://$CI_USER_TOKEN@github.com/optimizely/travisci-tools.git && popd
- script:
- - $HOME/travisci-tools/trigger-script-with-status-update.sh
- after_success: travis_terminate 0
- - <<: *integrationtest
- stage: 'Benchmarking tests'
- env: SDK=java FULLSTACK_TEST_REPO=Benchmarking SDK_BRANCH=$TRAVIS_PULL_REQUEST_BRANCH
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b3a98575..565bfcd5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,320 @@
# Optimizely Java X SDK Changelog
+## [4.2.2]
+May 28th, 2025
+
+### Fixes
+- Added experimentId and variationId to decision notification ([#569](https://github.com/optimizely/java-sdk/pull/569)).
+
+## [4.2.1]
+Feb 19th, 2025
+
+### Fixes
+- Fix big integer conversion ([#556](https://github.com/optimizely/java-sdk/pull/556)).
+
+## [4.2.0]
+November 6th, 2024
+
+### New Features
+* Batch UPS lookup and save calls in decideAll and decideForKeys methods ([#549](https://github.com/optimizely/java-sdk/pull/549)).
+
+
+## [4.1.1]
+May 8th, 2024
+
+### Fixes
+- Fix logx events discarded for staled connections with httpclient connection pooling ([#545](https://github.com/optimizely/java-sdk/pull/545)).
+
+
+## [4.1.0]
+April 12th, 2024
+
+### New Features
+* OptimizelyFactory method for injecting customHttpClient is fixed to share the customHttpClient for all modules using httpClient (HttpProjectConfigManager, AsyncEventHander, ODPManager) ([#542](https://github.com/optimizely/java-sdk/pull/542)).
+* A custom ThreadFactory can be injected to support virtual threads (Loom) ([#540](https://github.com/optimizely/java-sdk/pull/540)).
+
+
+## [4.0.0]
+January 16th, 2024
+
+### New Features
+The 4.0.0 release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting)
+enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) (
+[#474](https://github.com/optimizely/java-sdk/pull/474),
+[#481](https://github.com/optimizely/java-sdk/pull/481),
+[#482](https://github.com/optimizely/java-sdk/pull/482),
+[#483](https://github.com/optimizely/java-sdk/pull/483),
+[#484](https://github.com/optimizely/java-sdk/pull/484),
+[#485](https://github.com/optimizely/java-sdk/pull/485),
+[#487](https://github.com/optimizely/java-sdk/pull/487),
+[#489](https://github.com/optimizely/java-sdk/pull/489),
+[#490](https://github.com/optimizely/java-sdk/pull/490),
+[#494](https://github.com/optimizely/java-sdk/pull/494)
+).
+
+You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex
+real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important
+for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can
+be used as a single source of truth for these segments in any Optimizely or 3rd party tool.
+
+With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and
+make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Optimizely Customer Success Manager.
+
+This version includes the following changes:
+- New API added to `OptimizelyUserContext`:
+ - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays.
+ - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities.
+- New APIs added to `OptimizelyClient`:
+ - `sendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP.
+
+For details, refer to our documentation pages:
+- [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting)
+- [Server SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-server-side-sdks)
+- [Initialize Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-java)
+- [OptimizelyUserContext Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-java)
+- [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-java)
+- [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-java)
+
+### Breaking Changes
+- `OdpManager` in the SDK is enabled by default, if initialized using OptimizelyFactory. Unless an ODP account is integrated into the Optimizely projects, most `OdpManager` functions will be ignored. If needed, ODP features can be disabled by initializing `OptimizelyClient` without passing `OdpManager`.
+- `ProjectConfigManager` interface has been changed to add 2 more methods `getCachedConfig()` and `getSDKKey()`. Custom ProjectConfigManager should implement these new methods. See `PollingProjectConfigManager` for reference. This change is required to support ODPManager updated on datafile download ([#501](https://github.com/optimizely/java-sdk/pull/501)).
+
+### Fixes
+- Fix thread leak from httpClient in HttpProjectConfigManager ([#530](https://github.com/optimizely/java-sdk/pull/530)).
+- Fix issue when vuid is passed as userid for `AsyncGetQualifiedSegments` ([#527](https://github.com/optimizely/java-sdk/pull/527)).
+- Fix to support arbitrary client names to be included in logx and odp events ([#524](https://github.com/optimizely/java-sdk/pull/524)).
+- Add evict timeout to logx connections ([#518](https://github.com/optimizely/java-sdk/pull/518)).
+
+### Functionality Enhancements
+- Update Github Issue Templates ([#531](https://github.com/optimizely/java-sdk/pull/531))
+
+
+
+## [4.0.0-beta2]
+August 28th, 2023
+
+### Fixes
+- Fix thread leak from httpClient in HttpProjectConfigManager ([#530](https://github.com/optimizely/java-sdk/pull/530)).
+- Fix issue when vuid is passed as userid for `AsyncGetQualifiedSegments` ([#527](https://github.com/optimizely/java-sdk/pull/527)).
+- Fix to support arbitrary client names to be included in logx and odp events ([#524](https://github.com/optimizely/java-sdk/pull/524)).
+
+### Functionality Enhancements
+- Update Github Issue Templates ([#531](https://github.com/optimizely/java-sdk/pull/531))
+
+
+## [3.10.4]
+June 8th, 2023
+
+### Fixes
+- Fix intermittent logx event dispatch failures possibly caused by reusing stale connections. Add `evictIdleConnections` (1min) to `OptimizelyHttpClient` in `AsyncEventHandler` to force close persistent connections after 1min idle time ([#518](https://github.com/optimizely/java-sdk/pull/518)).
+
+
+## [4.0.0-beta]
+May 5th, 2023
+
+### New Features
+The 4.0.0-beta release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting)
+enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) (
+[#474](https://github.com/optimizely/java-sdk/pull/474),
+[#481](https://github.com/optimizely/java-sdk/pull/481),
+[#482](https://github.com/optimizely/java-sdk/pull/482),
+[#483](https://github.com/optimizely/java-sdk/pull/483),
+[#484](https://github.com/optimizely/java-sdk/pull/484),
+[#485](https://github.com/optimizely/java-sdk/pull/485),
+[#487](https://github.com/optimizely/java-sdk/pull/487),
+[#489](https://github.com/optimizely/java-sdk/pull/489),
+[#490](https://github.com/optimizely/java-sdk/pull/490),
+[#494](https://github.com/optimizely/java-sdk/pull/494)
+).
+
+You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex
+real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important
+for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can
+be used as a single source of truth for these segments in any Optimizely or 3rd party tool.
+
+With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and
+make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Optimizely Customer Success Manager.
+
+This version includes the following changes:
+- New API added to `OptimizelyUserContext`:
+ - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays.
+ - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities.
+- New APIs added to `OptimizelyClient`:
+ - `sendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP.
+
+For details, refer to our documentation pages:
+- [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting)
+- [Server SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-server-side-sdks)
+- [Initialize Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-java)
+- [OptimizelyUserContext Java SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-java)
+- [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-java)
+- [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-java)
+
+### Breaking Changes
+- `OdpManager` in the SDK is enabled by default, if initialized using OptimizelyFactory. Unless an ODP account is integrated into the Optimizely projects, most `OdpManager` functions will be ignored. If needed, `OdpManager` to be disabled initialize `OptimizelyClient` without passing `OdpManager`.
+- `ProjectConfigManager` interface has been changed to add 2 more methods `getCachedConfig()` and `getSDKKey()`. Custom ProjectConfigManager should implement these new methods. See `PollingProjectConfigManager` for reference. This change is required to support ODPManager updated on datafile download ([#501](https://github.com/optimizely/java-sdk/pull/501)).
+
+## [3.10.3]
+March 13th, 2023
+
+### Fixes
+We updated our README.md and other non-functional code to reflect that this SDK supports both Optimizely Feature Experimentation and Optimizely Full Stack ([#506](https://github.com/optimizely/java-sdk/pull/506)).
+
+## [3.10.2]
+March 17th, 2022
+
+### Fixes
+
+- For some audience condition matchers (semantic-version, le, or ge), SDK logs WARNING messages when the attribute value is missing. This is fixed down to the DEBUG level to be consistent with other condition matchers ([#463](https://github.com/optimizely/java-sdk/pull/463)).
+- Add an option to specify the client-engine version (android-sdk, etc) in the Optimizely builder ([#466](https://github.com/optimizely/java-sdk/pull/466)).
+
+
+## [3.10.1]
+February 3rd, 2022
+
+### Fixes
+- Fix NotificationManager to be thread-safe (add-handler and send-notifications can happen concurrently) ([#460](https://github.com/optimizely/java-sdk/pull/460)).
+
+## [3.10.0]
+January 10th, 2022
+
+### New Features
+* Add a set of new APIs for overriding and managing user-level flag, experiment and delivery rule decisions. These methods can be used for QA and automated testing purposes. They are an extension of the OptimizelyUserContext interface ([#451](https://github.com/optimizely/java-sdk/pull/451), [#454](https://github.com/optimizely/java-sdk/pull/454), [#455](https://github.com/optimizely/java-sdk/pull/455), [#457](https://github.com/optimizely/java-sdk/pull/457))
+ - setForcedDecision
+ - getForcedDecision
+ - removeForcedDecision
+ - removeAllForcedDecisions
+
+- For details, refer to our documentation pages: [OptimizelyUserContext](https://docs.developers.optimizely.com/full-stack/v4.0/docs/optimizelyusercontext-java) and [Forced Decision methods](https://docs.developers.optimizely.com/full-stack/v4.0/docs/forced-decision-methods-java).
+
+## [3.9.0]
+September 16th, 2021
+
+### New Features
+* Added new public properties to OptimizelyConfig. [#434] (https://github.com/optimizely/java-sdk/pull/434), [#438] (https://github.com/optimizely/java-sdk/pull/438)
+ - sdkKey
+ - environmentKey
+ - attributes
+ - events
+ - audiences and audiences in OptimizelyExperiment
+ - experimentRules
+ - deliveryRules
+* OptimizelyFeature.experimentsMap of OptimizelyConfig is now deprecated. Please use OptimizelyFeature.experiment_rules and OptimizelyFeature.delivery_rules. [#440] (https://github.com/optimizely/java-sdk/pull/440)
+* For more information please refer to Optimizely documentation: [https://docs.developers.optimizely.com/full-stack/docs/optimizelyconfig-java]
+
+* Added custom closeableHttpClient for custom proxy support. [#441] (https://github.com/optimizely/java-sdk/pull/441)
+
+## [3.8.2]
+March 8th, 2021
+
+### Fixes
+- Fix intermittent SocketTimeout exceptions while downloading datafiles. Add configurable `evictIdleConnections` to `HttpProjectConfigManager` to force close persistent connections after the idle time (evict after 1min idle time by default) ([#431](https://github.com/optimizely/java-sdk/pull/431)).
+
+## [3.8.1]
+March 2nd, 2021
+
+Switch publish repository to MavenCentral (bintray/jcenter sunset)
+
+### Fixes
+- Fix app crashing when the rollout length is zero ([#423](https://github.com/optimizely/java-sdk/pull/423)).
+- Fix javadoc warnings ([#426](https://github.com/optimizely/java-sdk/pull/426)).
+
+
+## [3.8.0]
+February 3rd, 2021
+
+### New Features
+
+- Introducing a new primary interface for retrieving feature flag status, configuration and associated experiment decisions for users ([#406](https://github.com/optimizely/java-sdk/pull/406), [#415](https://github.com/optimizely/java-sdk/pull/415), [#417](https://github.com/optimizely/java-sdk/pull/417)). The new `OptimizelyUserContext` class is instantiated with `createUserContext` and exposes the following APIs to get `OptimizelyDecision`:
+
+ - setAttribute
+ - decide
+ - decideAll
+ - decideForKeys
+ - trackEvent
+
+- For details, refer to our documentation page: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/java-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/java-sdk).
+
+### Fixes
+- Close the closable response from apache httpclient ([#419](https://github.com/optimizely/java-sdk/pull/419))
+
+
+## [3.8.0-beta2]
+January 14th, 2021
+
+### Fixes:
+- Clone user-context before calling Optimizely decide ([#417](https://github.com/optimizely/java-sdk/pull/417))
+- Return reasons as a part of tuple in decision hierarchy ([#415](https://github.com/optimizely/java-sdk/pull/415))
+
+## [3.8.0-beta]
+December 14th, 2020
+
+### New Features
+
+- Introducing a new primary interface for retrieving feature flag status, configuration and associated experiment decisions for users. The new `OptimizelyUserContext` class is instantiated with `createUserContext` and exposes the following APIs ([#406](https://github.com/optimizely/java-sdk/pull/406)):
+
+ - setAttribute
+ - decide
+ - decideAll
+ - decideForKeys
+ - trackEvent
+
+- For details, refer to our documentation page: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/java-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/java-sdk).
+
+## [3.7.0]
+November 20th, 2020
+
+### New Features
+- Add support for upcoming application-controlled introduction of tracking for non-experiment Flag decisions. ([#405](https://github.com/optimizely/java-sdk/pull/405), [#409](https://github.com/optimizely/java-sdk/pull/409), [#410](https://github.com/optimizely/java-sdk/pull/410))
+
+### Fixes:
+- Upgrade httpclient to 4.5.13
+
+## [3.6.0]
+September 30th, 2020
+
+### New Features
+- Add support for version audience condition which follows the semantic version (http://semver.org)[#386](https://github.com/optimizely/java-sdk/pull/386).
+
+- Add support for datafile accessor [#392](https://github.com/optimizely/java-sdk/pull/392).
+
+- Audience logging refactor (move from info to debug) [#380](https://github.com/optimizely/java-sdk/pull/380).
+
+- Added SetDatafileAccessToken method in OptimizelyFactory [#384](https://github.com/optimizely/java-sdk/pull/384).
+
+- Add MatchRegistry for custom match implementations. [#390] (https://github.com/optimizely/java-sdk/pull/390).
+
+### Fixes:
+- logging issue in quick-start application [#402] (https://github.com/optimizely/java-sdk/pull/402).
+
+- Update libraries to latest to avoid vulnerability issues [#397](https://github.com/optimizely/java-sdk/pull/397).
+
+## [3.5.0]
+July 7th, 2020
+
+### New Features
+- Add support for JSON feature variables ([#372](https://github.com/optimizely/java-sdk/pull/372), [#371](https://github.com/optimizely/java-sdk/pull/371), [#375](https://github.com/optimizely/java-sdk/pull/375))
+- Add support for authenticated datafile access ([#378](https://github.com/optimizely/java-sdk/pull/378))
+
+### Bug Fixes:
+- Adjust log level on audience evaluation logs ([#377](https://github.com/optimizely/java-sdk/pull/377))
+
+## [3.5.0-beta]
+July 2nd, 2020
+
+### New Features
+- Add support for JSON feature variables ([#372](https://github.com/optimizely/java-sdk/pull/372), [#371](https://github.com/optimizely/java-sdk/pull/371), [#375](https://github.com/optimizely/java-sdk/pull/375))
+- Add support for authenticated datafile access ([#378](https://github.com/optimizely/java-sdk/pull/378))
+
+### Bug Fixes:
+- Adjust log level on audience evaluation logs ([#377](https://github.com/optimizely/java-sdk/pull/377))
+
+## [3.4.3]
+April 28th, 2020
+
+### Bug Fixes:
+- Change FeatureVariable type from enum to string for forward compatibility. ([#370](https://github.com/optimizely/java-sdk/pull/370))
+
## [3.4.2]
March 30th, 2020
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ac1ec3884..640239efa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,7 +9,7 @@ We welcome contributions and feedback! All contributors must sign our [Contribut
3. Make sure to add tests!
4. Run `./gradlew clean check` to automatically catch potential bugs.
5. `git push` your changes to GitHub.
-6. Open a PR from your fork into the master branch of the original repoOpen a PR from your fork into the master branch of the original repo
+6. Open a PR from your fork into the master branch of the original repo.
7. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `master`.
8. Open a pull request from `YOUR_NAME/branch_name` to `master`.
9. A repository maintainer will review your pull request and, if all goes well, squash and merge it!
diff --git a/LICENSE b/LICENSE
index afc550977..c9f7279d1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2016, Optimizely
+ Copyright 2016-2024, Optimizely, Inc. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index ddbd5d59d..1a7370c43 100644
--- a/README.md
+++ b/README.md
@@ -1,84 +1,81 @@
-Optimizely Java SDK
-===================
-[](https://travis-ci.org/optimizely/java-sdk)
+# Optimizely Java SDK
+
[](http://www.apache.org/licenses/LICENSE-2.0)
-This repository houses the Java SDK for use with Optimizely Full Stack and Optimizely Rollouts.
+This repository houses the Java SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy).
+
+Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at [Optimizely.com](https://www.optimizely.com/products/experiment/feature-experimentation/), or see the [developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome).
+
+Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feature-flagging/) for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap.
+
+## Get started
+
+Refer to the [Java SDK's developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/java-sdk) for detailed instructions on getting started with using the SDK.
+
+### Requirements
+
+Java 8 or higher versions.
-Optimizely Full Stack is A/B testing and feature flag management for product development teams. Experiment in any application. Make every feature on your roadmap an opportunity to learn. Learn more at https://www.optimizely.com/platform/full-stack/, or see the [documentation](https://docs.developers.optimizely.com/full-stack/docs).
+### Install the SDK
-Optimizely Rollouts is free feature flags for development teams. Easily roll out and roll back features in any application without code deploys. Mitigate risk for every feature on your roadmap. Learn more at https://www.optimizely.com/rollouts/, or see the [documentation](https://docs.developers.optimizely.com/rollouts/docs).
+The Java SDK is distributed through Maven Central and is created with source and target compatibility of Java 1.8. The `core-api` and `httpclient` packages are [optimizely-sdk-core-api](https://mvnrepository.com/artifact/com.optimizely.ab/core-api) and [optimizely-sdk-httpclient](https://mvnrepository.com/artifact/com.optimizely.ab/core-httpclient-impl), respectively.
-## Getting Started
+`core-api` requires [org.slf4j:slf4j-api:1.7.16](https://mvnrepository.com/artifact/org.slf4j/slf4j-api/1.7.16) and a supported JSON parser.
+We currently integrate with [Jackson](https://github.com/FasterXML/jackson), [GSON](https://github.com/google/gson), [json.org](http://www.json.org), and [json-simple](https://code.google.com/archive/p/json-simple); if any of those packages are available at runtime, they will be used by `core-api`. If none of those packages are already provided in your project's classpath, one will need to be added.
-### Installing the SDK
+`core-httpclient-impl` is an optional dependency that implements the event dispatcher and requires [org.apache.httpcomponents:httpclient:4.5.2](https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient/4.5.2).
-#### Gradle
+---
-The SDK is available through Bintray and is created with source and target compatibility of 1.8. The core-api and httpclient Bintray packages are [optimizely-sdk-core-api](https://bintray.com/optimizely/optimizely/optimizely-sdk-core-api)
-and [optimizely-sdk-httpclient](https://bintray.com/optimizely/optimizely/optimizely-sdk-httpclient) respectively. To install, place the
-following in your `build.gradle` and substitute `VERSION` for the latest SDK version available via Bintray:
+**NOTE**
+
+Optimizely previously distributed the Java SDK through Bintray/JCenter. But, as of April 27, 2021, [Bintray/JCenter will become a read-only repository indefinitely](https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/). The publish repository has been migrated to [MavenCentral](https://mvnrepository.com/artifact/com.optimizely.ab) for the SDK version 3.8.1 or later.
+
+---
```
repositories {
+ mavenCentral()
jcenter()
}
dependencies {
compile 'com.optimizely.ab:core-api:{VERSION}'
compile 'com.optimizely.ab:core-httpclient-impl:{VERSION}'
- // The SDK integrates with multiple JSON parsers, here we use
- // Jackson.
+ // The SDK integrates with multiple JSON parsers, here we use Jackson.
compile 'com.fasterxml.jackson.core:jackson-core:2.7.1'
compile 'com.fasterxml.jackson.core:jackson-annotations:2.7.1'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1'
}
-```
-
-#### Dependencies
-
-`core-api` requires [org.slf4j:slf4j-api:1.7.16](https://mvnrepository.com/artifact/org.slf4j/slf4j-api/1.7.16) and a supported JSON parser.
-We currently integrate with [Jackson](https://github.com/FasterXML/jackson), [GSON](https://github.com/google/gson), [json.org](http://www.json.org),
-and [json-simple](https://code.google.com/archive/p/json-simple); if any of those packages are available at runtime, they will be used by `core-api`.
-If none of those packages are already provided in your project's classpath, one will need to be added. `core-httpclient-impl` is an optional
-dependency that implements the event dispatcher and requires [org.apache.httpcomponents:httpclient:4.5.2](https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient/4.5.2).
-The supplied `pom` files on Bintray define module dependencies.
-
-### Feature Management Access
-To access the Feature Management configuration in the Optimizely dashboard, please contact your Optimizely account executive.
-
-### Using the SDK
+```
-See the Optimizely Full Stack [developer documentation](http://developers.optimizely.com/server/reference/index.html) to learn how to set
-up your first Java project and use the SDK.
-## Development
+## Use the Java SDK
-### Building the SDK
+See the Optimizely Feature Experimentation [developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0-full-stack/docs/java-sdk) to learn how to set up your first Java project and use the SDK.
-To build local jars which are outputted into the respective modules' `build/lib` directories:
-```
-./gradlew build
-```
+## SDK Development
### Unit tests
-#### Running all tests
-
You can run all unit tests with:
```
+
./gradlew test
+
```
### Checking for bugs
-We utilize [FindBugs](http://findbugs.sourceforge.net/) to identify possible bugs in the SDK. To run the check:
+We utilize [SpotBugs](https://spotbugs.github.io/) to identify possible bugs in the SDK. To run the check:
```
+
./gradlew check
+
```
### Benchmarking
@@ -86,7 +83,9 @@ We utilize [FindBugs](http://findbugs.sourceforge.net/) to identify possible bug
[JMH](http://openjdk.java.net/projects/code-tools/jmh/) benchmarks can be run through gradle:
```
+
./gradlew core-api:jmh
+
```
Results are generated in `$buildDir/reports/jmh`.
@@ -105,34 +104,75 @@ This software incorporates code from the following open source projects:
#### core-api module
-**SLF4J** [https://www.slf4j.org ](https://www.slf4j.org)
-Copyright © 2004-2017 QOS.ch
+**SLF4J** [https://www.slf4j.org ](https://www.slf4j.org)
+
+Copyright © 2004-2017 QOS.ch
+
License (MIT): [https://www.slf4j.org/license.html](https://www.slf4j.org/license.html)
-**Jackson Annotations** [https://github.com/FasterXML/jackson-annotations](https://github.com/FasterXML/jackson-annotations)
+**Jackson Annotations** [https://github.com/FasterXML/jackson-annotations](https://github.com/FasterXML/jackson-annotations)
+
License (Apache 2.0): [https://github.com/FasterXML/jackson-annotations/blob/master/src/main/resources/META-INF/LICENSE](https://github.com/FasterXML/jackson-annotations/blob/master/src/main/resources/META-INF/LICENSE)
-**Gson** [https://github.com/google/gson ](https://github.com/google/gson)
+**Gson** [https://github.com/google/gson ](https://github.com/google/gson)
+
Copyright © 2008 Google Inc.
+
License (Apache 2.0): [https://github.com/google/gson/blob/master/LICENSE](https://github.com/google/gson/blob/master/LICENSE)
-**JSON-java** [https://github.com/stleary/JSON-java](https://github.com/stleary/JSON-java)
-Copyright © 2002 JSON.org
+**JSON-java** [https://github.com/stleary/JSON-java](https://github.com/stleary/JSON-java)
+
+Copyright © 2002 JSON.org
+
License (The JSON License): [https://github.com/stleary/JSON-java/blob/master/LICENSE](https://github.com/stleary/JSON-java/blob/master/LICENSE)
-**JSON.simple** [https://code.google.com/archive/p/json-simple/](https://code.google.com/archive/p/json-simple/)
-Copyright © January 2004
+**JSON.simple** [https://code.google.com/archive/p/json-simple/](https://code.google.com/archive/p/json-simple/)
+
+Copyright © January 2004
+
License (Apache 2.0): [https://github.com/fangyidong/json-simple/blob/master/LICENSE.txt](https://github.com/fangyidong/json-simple/blob/master/LICENSE.txt)
-**Jackson Databind** [https://github.com/FasterXML/jackson-databind](https://github.com/FasterXML/jackson-databind)
+**Jackson Databind** [https://github.com/FasterXML/jackson-databind](https://github.com/FasterXML/jackson-databind)
+
License (Apache 2.0): [https://github.com/FasterXML/jackson-databind/blob/master/src/main/resources/META-INF/LICENSE](https://github.com/FasterXML/jackson-databind/blob/master/src/main/resources/META-INF/LICENSE)
#### core-httpclient-impl module
-**Gson** [https://github.com/google/gson ](https://github.com/google/gson)
+**Gson** [https://github.com/google/gson ](https://github.com/google/gson)
+
Copyright © 2008 Google Inc.
+
License (Apache 2.0): [https://github.com/google/gson/blob/master/LICENSE](https://github.com/google/gson/blob/master/LICENSE)
-**Apache HttpClient** [https://hc.apache.org/httpcomponents-client-ga/index.html ](https://hc.apache.org/httpcomponents-client-ga/index.html)
+**Apache HttpClient** [https://hc.apache.org/httpcomponents-client-ga/index.html ](https://hc.apache.org/httpcomponents-client-ga/index.html)
+
Copyright © January 2004
+
License (Apache 2.0): [https://github.com/apache/httpcomponents-client/blob/master/LICENSE.txt](https://github.com/apache/httpcomponents-client/blob/master/LICENSE.txt)
+
+### Other Optimzely SDKs
+
+- Agent - https://github.com/optimizely/agent
+
+- Android - https://github.com/optimizely/android-sdk
+
+- C# - https://github.com/optimizely/csharp-sdk
+
+- Flutter - https://github.com/optimizely/optimizely-flutter-sdk
+
+- Go - https://github.com/optimizely/go-sdk
+
+- Java - https://github.com/optimizely/java-sdk
+
+- JavaScript - https://github.com/optimizely/javascript-sdk
+
+- PHP - https://github.com/optimizely/php-sdk
+
+- Python - https://github.com/optimizely/python-sdk
+
+- React - https://github.com/optimizely/react-sdk
+
+- Ruby - https://github.com/optimizely/ruby-sdk
+
+- Swift - https://github.com/optimizely/swift-sdk
+
diff --git a/build.gradle b/build.gradle
index 5f6d659fa..54426f6e7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,26 +1,14 @@
-buildscript {
- repositories {
- jcenter()
- maven {
- url "https://oss.sonatype.org/content/repositories/snapshots/"
- }
- }
-
- dependencies {
- classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6'
- }
-}
-
plugins {
- id 'com.github.kt3k.coveralls' version '2.8.2'
+ id 'com.github.kt3k.coveralls' version '2.12.2'
id 'jacoco'
- id 'me.champeau.gradle.jmh' version '0.4.5'
+ id 'me.champeau.gradle.jmh' version '0.5.3'
id 'nebula.optional-base' version '3.2.0'
- id 'com.github.hierynomus.license' version '0.15.0'
+ id 'com.github.hierynomus.license' version '0.16.1'
+ id 'com.github.spotbugs' version "6.0.14"
+ id 'maven-publish'
}
allprojects {
- group = 'com.optimizely.ab'
apply plugin: 'idea'
apply plugin: 'jacoco'
@@ -29,25 +17,29 @@ allprojects {
}
jacoco {
- toolVersion = '0.8.0'
+ toolVersion = '0.8.7'
}
}
-apply from: 'gradle/publish.gradle'
-
allprojects {
- def travis_defined_version = System.getenv('TRAVIS_TAG')
+ group = 'com.optimizely.ab'
+
+ def travis_defined_version = System.getenv('GITHUB_TAG')
if (travis_defined_version != null) {
version = travis_defined_version
}
+
+ ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
}
-subprojects {
- apply plugin: 'com.jfrog.bintray'
- apply plugin: 'findbugs'
+def publishedProjects = subprojects.findAll { it.name != 'java-quickstart' }
+
+configure(publishedProjects) {
+ apply plugin: 'com.github.spotbugs'
apply plugin: 'jacoco'
apply plugin: 'java'
apply plugin: 'maven-publish'
+ apply plugin: 'signing'
apply plugin: 'me.champeau.gradle.jmh'
apply plugin: 'nebula.optional-base'
apply plugin: 'com.github.hierynomus.license'
@@ -57,32 +49,31 @@ subprojects {
repositories {
jcenter()
+ maven {
+ url 'https://plugins.gradle.org/m2/'
+ }
}
task sourcesJar(type: Jar, dependsOn: classes) {
- classifier = 'sources'
+ archiveClassifier.set('sources')
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
- classifier = 'javadoc'
+ archiveClassifier.set('javadoc')
from javadoc.destinationDir
}
- artifacts {
- archives sourcesJar
- archives javadocJar
- }
-
- tasks.withType(FindBugs) {
+ spotbugsMain {
reports {
xml.enabled = false
html.enabled = true
}
}
- findbugs {
- findbugsJmh.enabled = false
+ spotbugs {
+ spotbugsJmh.enabled = false
+ reportLevel = com.github.spotbugs.snom.Confidence.valueOf('HIGH')
}
test {
@@ -105,49 +96,71 @@ subprojects {
}
dependencies {
- testCompile group: 'junit', name: 'junit', version: junitVersion
- testCompile group: 'org.mockito', name: 'mockito-core', version: mockitoVersion
- testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: hamcrestVersion
- testCompile group: 'com.google.guava', name: 'guava', version: guavaVersion
+ implementation group: 'commons-codec', name: 'commons-codec', version: commonCodecVersion
+
+ testImplementation group: 'junit', name: 'junit', version: junitVersion
+ testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion
+ testImplementation group: 'org.hamcrest', name: 'hamcrest-all', version: hamcrestVersion
+ testImplementation group: 'com.google.guava', name: 'guava', version: guavaVersion
// logging dependencies (logback)
- testCompile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
- testCompile group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion
-
- testCompile group: 'com.google.code.gson', name: 'gson', version: gsonVersion
- testCompile group: 'org.json', name: 'json', version: jsonVersion
- testCompile group: 'com.googlecode.json-simple', name: 'json-simple', version: jsonSimpleVersion
- testCompile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion
- }
-
- publishing {
- publications {
- mavenJava(MavenPublication) {
- from components.java
- artifact sourcesJar
- artifact javadocJar
- pom.withXml {
- asNode().children().last() + {
- resolveStrategy = Closure.DELEGATE_FIRST
- url 'https://github.com/optimizely/java-sdk'
- licenses {
- license {
- name 'The Apache Software License, Version 2.0'
- url 'http://www.apache.org/license/LICENSE-2.0.txt'
- distribution 'repo'
- }
- }
- developers {
- developer {
- id 'optimizely'
- name 'Optimizely'
- email 'developers@optimizely.com'
- }
- }
+ testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
+ testImplementation group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion
+
+ testImplementation group: 'com.google.code.gson', name: 'gson', version: gsonVersion
+ testImplementation group: 'org.json', name: 'json', version: jsonVersion
+ testImplementation group: 'com.googlecode.json-simple', name: 'json-simple', version: jsonSimpleVersion
+ testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion
+ }
+
+ configurations.all {
+ resolutionStrategy {
+ force "junit:junit:${junitVersion}"
+ }
+ }
+
+
+ def docTitle = "Optimizely Java SDK"
+ if (name.equals('core-httpclient-impl')) {
+ docTitle = "Optimizely Java SDK: Httpclient"
+ }
+
+ afterEvaluate {
+ publishing {
+ publications {
+ release(MavenPublication) {
+ customizePom(pom, docTitle)
+
+ from components.java
+ artifact sourcesJar
+ artifact javadocJar
+ }
+ }
+ repositories {
+ maven {
+ def releaseUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
+ def snapshotUrl = "https://oss.sonatype.org/content/repositories/snapshots"
+ url = isReleaseVersion ? releaseUrl : snapshotUrl
+ credentials {
+ username System.getenv('MAVEN_CENTRAL_USERNAME')
+ password System.getenv('MAVEN_CENTRAL_PASSWORD')
}
}
}
}
+
+ signing {
+ // base64 for workaround travis escape chars issue
+ def signingKeyBase64 = System.getenv('MAVEN_SIGNING_KEY_BASE64')
+ // skip signing for "local" version into MavenLocal for test-app
+ if (!signingKeyBase64?.trim()) return
+ byte[] decoded = signingKeyBase64.decodeBase64()
+ def signingKey = new String(decoded)
+
+ def signingPassword = System.getenv('MAVEN_SIGNING_PASSPHRASE')
+ useInMemoryPgpKeys(signingKey, signingPassword)
+ sign publishing.publications.release
+ }
}
license {
@@ -158,42 +171,20 @@ subprojects {
ext.year = Calendar.getInstance().get(Calendar.YEAR)
}
- def bintrayName = 'core-api';
- if (name.equals('core-httpclient-impl')) {
- bintrayName = 'httpclient'
- }
-
- bintray {
- user = System.getenv('BINTRAY_USER')
- key = System.getenv('BINTRAY_KEY')
- pkg {
- repo = 'optimizely'
- name = "optimizely-sdk-${bintrayName}"
- userOrg = 'optimizely'
- version {
- name = rootProject.version
- }
- publications = ['mavenJava']
- }
- }
-
- build.dependsOn('generatePomFileForMavenJavaPublication')
-
- bintrayUpload.dependsOn 'build'
-
task ship() {
- dependsOn('bintrayUpload')
+ dependsOn('publish')
}
+ // concurrent publishing (maven-publish) causes an issue with maven-central repository
+ // - a single module splits into multiple staging repos, so validation fails.
+ // - adding this ordering requirement forces sequential publishing processes.
+ project(':core-api').javadocJar.mustRunAfter = [':core-httpclient-impl:ship']
}
task ship() {
dependsOn(':core-api:ship', ':core-httpclient-impl:ship')
}
-// Only report code coverage for projects that are distributed
-def publishedProjects = subprojects.findAll { it.path != ':simulator' }
-
task jacocoMerge(type: JacocoMerge) {
publishedProjects.each { subproject ->
executionData subproject.tasks.withType(Test)
@@ -207,9 +198,9 @@ task jacocoRootReport(type: JacocoReport, group: 'Coverage reports') {
description = 'Generates an aggregate report from all subprojects'
dependsOn publishedProjects.test, jacocoMerge
- additionalSourceDirs = files(publishedProjects.sourceSets.main.allSource.srcDirs)
- sourceDirectories = files(publishedProjects.sourceSets.main.allSource.srcDirs)
- classDirectories = files(publishedProjects.sourceSets.main.output)
+ getAdditionalSourceDirs().setFrom(files(publishedProjects.sourceSets.main.allSource.srcDirs))
+ getSourceDirectories().setFrom(files(publishedProjects.sourceSets.main.allSource.srcDirs))
+ getAdditionalClassDirs().setFrom(files(publishedProjects.sourceSets.main.output))
executionData jacocoMerge.destinationFile
reports {
@@ -230,3 +221,37 @@ tasks.coveralls {
dependsOn jacocoRootReport
onlyIf { System.env.'CI' && !JavaVersion.current().isJava9Compatible() }
}
+
+// standard POM format required by MavenCentral
+
+def customizePom(pom, title) {
+ pom.withXml {
+ asNode().children().last() + {
+ // keep this - otherwise some properties are not made into pom properly
+ resolveStrategy = Closure.DELEGATE_FIRST
+
+ name title
+ url 'https://github.com/optimizely/java-sdk'
+ description 'The Java SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts'
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+ developers {
+ developer {
+ id 'optimizely'
+ name 'Optimizely'
+ email 'optimizely-fullstack@optimizely.com'
+ }
+ }
+ scm {
+ connection 'scm:git:git://github.com/optimizely/java-sdk.git'
+ developerConnection 'scm:git:ssh:github.com/optimizely/java-sdk.git'
+ url 'https://github.com/optimizely/java-sdk.git'
+ }
+ }
+ }
+}
diff --git a/core-api/README.md b/core-api/README.md
index 13504566f..91d439ec7 100644
--- a/core-api/README.md
+++ b/core-api/README.md
@@ -1,7 +1,7 @@
# Java SDK Core API
-This package contains the core APIs and interfaces for the Optimizely Full Stack API in Java.
+This package contains the core APIs and interfaces for the Optimizely Feature Experimentation API in Java.
-Full product documentation is in the [Optimizely developers documentation](https://docs.developers.optimizely.com/full-stack/docs/welcome).
+Full product documentation is in the [Optimizely developers documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome).
## Installation
@@ -22,7 +22,7 @@ compile 'com.optimizely.ab:core-api:{VERSION}'
## Optimizely
[`Optimizely`](https://github.com/optimizely/java-sdk/blob/master/core-api/src/main/java/com/optimizely/ab/Optimizely.java)
-provides top level API access to the Full Stack project.
+provides top level API access to the Feature Experimentation project.
### Usage
```Java
diff --git a/core-api/build.gradle b/core-api/build.gradle
index d2609a97d..602131cd3 100644
--- a/core-api/build.gradle
+++ b/core-api/build.gradle
@@ -1,9 +1,10 @@
dependencies {
- compile group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion
- compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion
-
- compile group: 'com.google.code.findbugs', name: 'annotations', version: findbugsAnnotationVersion
- compile group: 'com.google.code.findbugs', name: 'jsr305', version: findbugsJsrVersion
+ implementation group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion
+ implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion
+ implementation group: 'com.google.code.findbugs', name: 'annotations', version: findbugsAnnotationVersion
+ implementation group: 'com.google.code.findbugs', name: 'jsr305', version: findbugsJsrVersion
+ testImplementation group: 'junit', name: 'junit', version: junitVersion
+ testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
// an assortment of json parsers
compileOnly group: 'com.google.code.gson', name: 'gson', version: gsonVersion, optional
@@ -12,6 +13,11 @@ dependencies {
compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion, optional
}
+tasks.named('processJmhResources') {
+ duplicatesStrategy = DuplicatesStrategy.WARN
+}
+
+
test {
useJUnit {
excludeCategories 'com.optimizely.ab.categories.ExhaustiveTest'
@@ -24,6 +30,7 @@ task exhaustiveTest(type: Test) {
}
}
+
task generateVersionFile {
// add the build version information into a file that'll go into the distribution
ext.buildVersion = new File(projectDir, "src/main/resources/optimizely-build-version")
diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java
index 39690a82e..6eead11c6 100644
--- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java
+++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java
@@ -1,5 +1,5 @@
/****************************************************************************
- * Copyright 2016-2020, Optimizely, Inc. and contributors *
+ * Copyright 2016-2024, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
@@ -20,17 +20,54 @@
import com.optimizely.ab.bucketing.DecisionService;
import com.optimizely.ab.bucketing.FeatureDecision;
import com.optimizely.ab.bucketing.UserProfileService;
-import com.optimizely.ab.config.*;
+import com.optimizely.ab.config.AtomicProjectConfigManager;
+import com.optimizely.ab.config.DatafileProjectConfig;
+import com.optimizely.ab.config.EventType;
+import com.optimizely.ab.config.Experiment;
+import com.optimizely.ab.config.FeatureFlag;
+import com.optimizely.ab.config.FeatureVariable;
+import com.optimizely.ab.config.FeatureVariableUsageInstance;
+import com.optimizely.ab.config.ProjectConfig;
+import com.optimizely.ab.config.ProjectConfigManager;
+import com.optimizely.ab.config.Variation;
import com.optimizely.ab.config.parser.ConfigParseException;
import com.optimizely.ab.error.ErrorHandler;
import com.optimizely.ab.error.NoOpErrorHandler;
-import com.optimizely.ab.event.*;
-import com.optimizely.ab.event.internal.*;
+import com.optimizely.ab.event.EventHandler;
+import com.optimizely.ab.event.EventProcessor;
+import com.optimizely.ab.event.ForwardingEventProcessor;
+import com.optimizely.ab.event.LogEvent;
+import com.optimizely.ab.event.NoopEventHandler;
+import com.optimizely.ab.event.internal.BuildVersionInfo;
+import com.optimizely.ab.event.internal.ClientEngineInfo;
+import com.optimizely.ab.event.internal.EventFactory;
+import com.optimizely.ab.event.internal.UserEvent;
+import com.optimizely.ab.event.internal.UserEventFactory;
import com.optimizely.ab.event.internal.payload.EventBatch;
-import com.optimizely.ab.notification.*;
+import com.optimizely.ab.internal.NotificationRegistry;
+import com.optimizely.ab.notification.ActivateNotification;
+import com.optimizely.ab.notification.DecisionNotification;
+import com.optimizely.ab.notification.FeatureTestSourceInfo;
+import com.optimizely.ab.notification.NotificationCenter;
+import com.optimizely.ab.notification.NotificationHandler;
+import com.optimizely.ab.notification.RolloutSourceInfo;
+import com.optimizely.ab.notification.SourceInfo;
+import com.optimizely.ab.notification.TrackNotification;
+import com.optimizely.ab.notification.UpdateConfigNotification;
+import com.optimizely.ab.odp.ODPEvent;
+import com.optimizely.ab.odp.ODPManager;
+import com.optimizely.ab.odp.ODPSegmentManager;
+import com.optimizely.ab.odp.ODPSegmentOption;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService;
+import com.optimizely.ab.optimizelydecision.DecisionMessage;
+import com.optimizely.ab.optimizelydecision.DecisionReasons;
+import com.optimizely.ab.optimizelydecision.DecisionResponse;
+import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
+import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
+import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
+import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,22 +76,24 @@
import javax.annotation.concurrent.ThreadSafe;
import java.io.Closeable;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
import static com.optimizely.ab.internal.SafetyUtils.tryClose;
/**
* Top-level container class for Optimizely functionality.
* Thread-safe, so can be created as a singleton and safely passed around.
- *
+ *
* Example instantiation:
*
* Optimizely optimizely = Optimizely.builder(projectWatcher, eventHandler).build();
*
- *
+ *
* To activate an experiment and perform variation specific processing:
*
* Variation variation = optimizely.activate(experimentKey, userId, attributes);
@@ -76,9 +115,7 @@ public class Optimizely implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(Optimizely.class);
- @VisibleForTesting
final DecisionService decisionService;
- @VisibleForTesting
@Deprecated
final EventHandler eventHandler;
@VisibleForTesting
@@ -86,7 +123,10 @@ public class Optimizely implements AutoCloseable {
@VisibleForTesting
final ErrorHandler errorHandler;
- private final ProjectConfigManager projectConfigManager;
+ public final List defaultDecideOptions;
+
+ @VisibleForTesting
+ final ProjectConfigManager projectConfigManager;
@Nullable
private final OptimizelyConfigManager optimizelyConfigManager;
@@ -97,6 +137,11 @@ public class Optimizely implements AutoCloseable {
@Nullable
private final UserProfileService userProfileService;
+ @Nullable
+ private final ODPManager odpManager;
+
+ private final ReentrantLock lock = new ReentrantLock();
+
private Optimizely(@Nonnull EventHandler eventHandler,
@Nonnull EventProcessor eventProcessor,
@Nonnull ErrorHandler errorHandler,
@@ -104,7 +149,9 @@ private Optimizely(@Nonnull EventHandler eventHandler,
@Nullable UserProfileService userProfileService,
@Nonnull ProjectConfigManager projectConfigManager,
@Nullable OptimizelyConfigManager optimizelyConfigManager,
- @Nonnull NotificationCenter notificationCenter
+ @Nonnull NotificationCenter notificationCenter,
+ @Nonnull List defaultDecideOptions,
+ @Nullable ODPManager odpManager
) {
this.eventHandler = eventHandler;
this.eventProcessor = eventProcessor;
@@ -114,6 +161,23 @@ private Optimizely(@Nonnull EventHandler eventHandler,
this.projectConfigManager = projectConfigManager;
this.optimizelyConfigManager = optimizelyConfigManager;
this.notificationCenter = notificationCenter;
+ this.defaultDecideOptions = defaultDecideOptions;
+ this.odpManager = odpManager;
+
+ if (odpManager != null) {
+ odpManager.getEventManager().start();
+ if (projectConfigManager.getCachedConfig() != null) {
+ updateODPSettings();
+ }
+ if (projectConfigManager.getSDKKey() != null) {
+ NotificationRegistry.getInternalNotificationCenter(projectConfigManager.getSDKKey()).
+ addNotificationHandler(UpdateConfigNotification.class,
+ configNotification -> {
+ updateODPSettings();
+ });
+ }
+
+ }
}
/**
@@ -127,8 +191,6 @@ public boolean isValid() {
return getProjectConfig() != null;
}
-
-
/**
* Checks if eventHandler {@link EventHandler} and projectConfigManager {@link ProjectConfigManager}
* are Closeable {@link Closeable} and calls close on them.
@@ -140,6 +202,11 @@ public void close() {
tryClose(eventProcessor);
tryClose(eventHandler);
tryClose(projectConfigManager);
+ notificationCenter.clearAllNotificationListeners();
+ NotificationRegistry.clearNotificationCenterRegistry(projectConfigManager.getSDKKey());
+ if (odpManager != null) {
+ tryClose(odpManager);
+ }
}
//======== activate calls ========//
@@ -216,31 +283,67 @@ private Variation activate(@Nullable ProjectConfig projectConfig,
return null;
}
- sendImpression(projectConfig, experiment, userId, copiedAttributes, variation);
+ sendImpression(projectConfig, experiment, userId, copiedAttributes, variation, "experiment");
return variation;
}
+ /**
+ * Creates and sends impression event.
+ *
+ * @param projectConfig the current projectConfig
+ * @param experiment the experiment user bucketed into and dispatch an impression event
+ * @param userId the ID of the user
+ * @param filteredAttributes the attributes of the user
+ * @param variation the variation that was returned from activate.
+ * @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
+ */
private void sendImpression(@Nonnull ProjectConfig projectConfig,
@Nonnull Experiment experiment,
@Nonnull String userId,
@Nonnull Map filteredAttributes,
- @Nonnull Variation variation) {
- if (!experiment.isRunning()) {
- logger.info("Experiment has \"Launched\" status so not dispatching event during activation.");
- return;
- }
+ @Nonnull Variation variation,
+ @Nonnull String ruleType) {
+ sendImpression(projectConfig, experiment, userId, filteredAttributes, variation, "", ruleType, true);
+ }
+
+ /**
+ * Creates and sends impression event.
+ *
+ * @param projectConfig the current projectConfig
+ * @param experiment the experiment user bucketed into and dispatch an impression event
+ * @param userId the ID of the user
+ * @param filteredAttributes the attributes of the user
+ * @param variation the variation that was returned from activate.
+ * @param flagKey It can either be empty if ruleType is experiment or it's feature key in case ruleType is feature-test or rollout
+ * @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
+ */
+ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
+ @Nullable Experiment experiment,
+ @Nonnull String userId,
+ @Nonnull Map filteredAttributes,
+ @Nullable Variation variation,
+ @Nonnull String flagKey,
+ @Nonnull String ruleType,
+ @Nonnull boolean enabled) {
UserEvent userEvent = UserEventFactory.createImpressionEvent(
projectConfig,
experiment,
variation,
userId,
- filteredAttributes);
+ filteredAttributes,
+ flagKey,
+ ruleType,
+ enabled);
+ if (userEvent == null) {
+ return false;
+ }
eventProcessor.process(userEvent);
- logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
-
+ if (experiment != null) {
+ logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
+ }
// Kept For backwards compatibility.
// This notification is deprecated and the new DecisionNotifications
// are sent via their respective method calls.
@@ -250,6 +353,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
experiment, userId, filteredAttributes, variation, impressionEvent);
notificationCenter.send(activateNotification);
}
+ return true;
}
//======== track calls ========//
@@ -335,7 +439,7 @@ public void track(@Nonnull String eventName,
@Nonnull
public Boolean isFeatureEnabled(@Nonnull String featureKey,
@Nonnull String userId) {
- return isFeatureEnabled(featureKey, userId, Collections.emptyMap());
+ return isFeatureEnabled(featureKey, userId, Collections.emptyMap());
}
/**
@@ -383,19 +487,17 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
Map copiedAttributes = copyAttributes(attributes);
FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT;
- FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig);
+ FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContextCopy(userId, copiedAttributes), projectConfig).getResult();
Boolean featureEnabled = false;
SourceInfo sourceInfo = new RolloutSourceInfo();
+ if (featureDecision.decisionSource != null) {
+ decisionSource = featureDecision.decisionSource;
+ }
if (featureDecision.variation != null) {
+ // This information is only necessary for feature tests.
+ // For rollouts experiments and variations are an implementation detail only.
if (featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) {
- sendImpression(
- projectConfig,
- featureDecision.experiment,
- userId,
- copiedAttributes,
- featureDecision.variation);
- decisionSource = featureDecision.decisionSource;
sourceInfo = new FeatureTestSourceInfo(featureDecision.experiment.getKey(), featureDecision.variation.getKey());
} else {
logger.info("The user \"{}\" is not included in an experiment for feature \"{}\".",
@@ -405,6 +507,15 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
featureEnabled = true;
}
}
+ sendImpression(
+ projectConfig,
+ featureDecision.experiment,
+ userId,
+ copiedAttributes,
+ featureDecision.variation,
+ featureKey,
+ decisionSource.toString(),
+ featureEnabled);
DecisionNotification decisionNotification = DecisionNotification.newFeatureDecisionNotificationBuilder()
.withUserId(userId)
@@ -561,6 +672,53 @@ public Integer getFeatureVariableInteger(@Nonnull String featureKey,
return variableValue;
}
+ /**
+ * Get the Long value of the specified variable in the feature.
+ *
+ * @param featureKey The unique key of the feature.
+ * @param variableKey The unique key of the variable.
+ * @param userId The ID of the user.
+ * @return The Integer value of the integer single variable feature.
+ * Null if the feature or variable could not be found.
+ */
+ @Nullable
+ public Long getFeatureVariableLong(@Nonnull String featureKey,
+ @Nonnull String variableKey,
+ @Nonnull String userId) {
+ return getFeatureVariableLong(featureKey, variableKey, userId, Collections.emptyMap());
+ }
+
+ /**
+ * Get the Integer value of the specified variable in the feature.
+ *
+ * @param featureKey The unique key of the feature.
+ * @param variableKey The unique key of the variable.
+ * @param userId The ID of the user.
+ * @param attributes The user's attributes.
+ * @return The Integer value of the integer single variable feature.
+ * Null if the feature or variable could not be found.
+ */
+ @Nullable
+ public Long getFeatureVariableLong(@Nonnull String featureKey,
+ @Nonnull String variableKey,
+ @Nonnull String userId,
+ @Nonnull Map attributes) {
+ try {
+ return getFeatureVariableValueForType(
+ featureKey,
+ variableKey,
+ userId,
+ attributes,
+ FeatureVariable.INTEGER_TYPE
+ );
+
+ } catch (Exception exception) {
+ logger.error("NumberFormatException while trying to parse value as Long. {}", String.valueOf(exception));
+ }
+
+ return null;
+ }
+
/**
* Get the String value of the specified variable in the feature.
*
@@ -601,12 +759,52 @@ public String getFeatureVariableString(@Nonnull String featureKey,
FeatureVariable.STRING_TYPE);
}
+ /**
+ * Get the JSON value of the specified variable in the feature.
+ *
+ * @param featureKey The unique key of the feature.
+ * @param variableKey The unique key of the variable.
+ * @param userId The ID of the user.
+ * @return An OptimizelyJSON instance for the JSON variable value.
+ * Null if the feature or variable could not be found.
+ */
+ @Nullable
+ public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey,
+ @Nonnull String variableKey,
+ @Nonnull String userId) {
+ return getFeatureVariableJSON(featureKey, variableKey, userId, Collections.emptyMap());
+ }
+
+ /**
+ * Get the JSON value of the specified variable in the feature.
+ *
+ * @param featureKey The unique key of the feature.
+ * @param variableKey The unique key of the variable.
+ * @param userId The ID of the user.
+ * @param attributes The user's attributes.
+ * @return An OptimizelyJSON instance for the JSON variable value.
+ * Null if the feature or variable could not be found.
+ */
+ @Nullable
+ public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey,
+ @Nonnull String variableKey,
+ @Nonnull String userId,
+ @Nonnull Map attributes) {
+
+ return getFeatureVariableValueForType(
+ featureKey,
+ variableKey,
+ userId,
+ attributes,
+ FeatureVariable.JSON_TYPE);
+ }
+
@VisibleForTesting
T getFeatureVariableValueForType(@Nonnull String featureKey,
- @Nonnull String variableKey,
- @Nonnull String userId,
- @Nonnull Map attributes,
- @Nonnull String variableType) {
+ @Nonnull String variableKey,
+ @Nonnull String userId,
+ @Nonnull Map attributes,
+ @Nonnull String variableType) {
if (featureKey == null) {
logger.warn("The featureKey parameter must be nonnull.");
return null;
@@ -645,7 +843,7 @@ T getFeatureVariableValueForType(@Nonnull String featureKey,
String variableValue = variable.getDefaultValue();
Map copiedAttributes = copyAttributes(attributes);
- FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig);
+ FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContextCopy(userId, copiedAttributes), projectConfig).getResult();
Boolean featureEnabled = false;
if (featureDecision.variation != null) {
if (featureDecision.variation.getFeatureEnabled()) {
@@ -653,13 +851,15 @@ T getFeatureVariableValueForType(@Nonnull String featureKey,
featureDecision.variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId());
if (featureVariableUsageInstance != null) {
variableValue = featureVariableUsageInstance.getValue();
+ logger.info("Got variable value \"{}\" for variable \"{}\" of feature flag \"{}\".", variableValue, variableKey, featureKey);
} else {
variableValue = variable.getDefaultValue();
+ logger.info("Value is not defined for variable \"{}\". Returning default value \"{}\".", variableKey, variableValue);
}
} else {
- logger.info("Feature \"{}\" for variation \"{}\" was not enabled. " +
- "The default value is being returned.",
- featureKey, featureDecision.variation.getKey(), variableValue, variableKey
+ logger.info("Feature \"{}\" is not enabled for user \"{}\". " +
+ "Returning the default variable value \"{}\".",
+ featureKey, userId, variableValue
);
}
featureEnabled = featureDecision.variation.getFeatureEnabled();
@@ -671,6 +871,10 @@ T getFeatureVariableValueForType(@Nonnull String featureKey,
}
Object convertedValue = convertStringToType(variableValue, variableType);
+ Object notificationValue = convertedValue;
+ if (convertedValue instanceof OptimizelyJSON) {
+ notificationValue = ((OptimizelyJSON) convertedValue).toMap();
+ }
DecisionNotification decisionNotification = DecisionNotification.newFeatureVariableDecisionNotificationBuilder()
.withUserId(userId)
@@ -679,7 +883,7 @@ T getFeatureVariableValueForType(@Nonnull String featureKey,
.withFeatureEnabled(featureEnabled)
.withVariableKey(variableKey)
.withVariableType(variableType)
- .withVariableValue(convertedValue)
+ .withVariableValue(notificationValue)
.withFeatureDecision(featureDecision)
.build();
@@ -690,7 +894,6 @@ T getFeatureVariableValueForType(@Nonnull String featureKey,
}
// Helper method which takes type and variable value and convert it to object to use in Listener DecisionInfo object variable value
- @VisibleForTesting
Object convertStringToType(String variableValue, String type) {
if (variableValue != null) {
switch (type) {
@@ -710,10 +913,17 @@ Object convertStringToType(String variableValue, String type) {
try {
return Integer.parseInt(variableValue);
} catch (NumberFormatException exception) {
- logger.error("NumberFormatException while trying to parse \"" + variableValue +
- "\" as Integer. " + exception.toString());
+ try {
+ return Long.parseLong(variableValue);
+ } catch (NumberFormatException longException) {
+ logger.error("NumberFormatException while trying to parse \"{}\" as Integer. {}",
+ variableValue,
+ exception.toString());
+ }
}
break;
+ case FeatureVariable.JSON_TYPE:
+ return new OptimizelyJSON(variableValue);
default:
return variableValue;
}
@@ -722,6 +932,103 @@ Object convertStringToType(String variableValue, String type) {
return null;
}
+ /**
+ * Get the values of all variables in the feature.
+ *
+ * @param featureKey The unique key of the feature.
+ * @param userId The ID of the user.
+ * @return An OptimizelyJSON instance for all variable values.
+ * Null if the feature could not be found.
+ */
+ @Nullable
+ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
+ @Nonnull String userId) {
+ return getAllFeatureVariables(featureKey, userId, Collections.emptyMap());
+ }
+
+ /**
+ * Get the values of all variables in the feature.
+ *
+ * @param featureKey The unique key of the feature.
+ * @param userId The ID of the user.
+ * @param attributes The user's attributes.
+ * @return An OptimizelyJSON instance for all variable values.
+ * Null if the feature could not be found.
+ */
+ @Nullable
+ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
+ @Nonnull String userId,
+ @Nonnull Map attributes) {
+
+ if (featureKey == null) {
+ logger.warn("The featureKey parameter must be nonnull.");
+ return null;
+ } else if (userId == null) {
+ logger.warn("The userId parameter must be nonnull.");
+ return null;
+ }
+
+ ProjectConfig projectConfig = getProjectConfig();
+ if (projectConfig == null) {
+ logger.error("Optimizely instance is not valid, failing getAllFeatureVariableValues call. type");
+ return null;
+ }
+
+ FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey);
+ if (featureFlag == null) {
+ logger.info("No feature flag was found for key \"{}\".", featureKey);
+ return null;
+ }
+
+ Map copiedAttributes = copyAttributes(attributes);
+ FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContextCopy(userId, copiedAttributes), projectConfig, Collections.emptyList()).getResult();
+ Boolean featureEnabled = false;
+ Variation variation = featureDecision.variation;
+
+ if (variation != null) {
+ featureEnabled = variation.getFeatureEnabled();
+ if (featureEnabled) {
+ logger.info("Feature \"{}\" is enabled for user \"{}\".", featureKey, userId);
+ } else {
+ logger.info("Feature \"{}\" is not enabled for user \"{}\".", featureKey, userId);
+ }
+ } else {
+ logger.info("User \"{}\" was not bucketed into any variation for feature flag \"{}\". " +
+ "The default values are being returned.", userId, featureKey);
+ }
+
+ Map valuesMap = new HashMap();
+ for (FeatureVariable variable : featureFlag.getVariables()) {
+ String value = variable.getDefaultValue();
+ if (featureEnabled) {
+ FeatureVariableUsageInstance instance = variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId());
+ if (instance != null) {
+ value = instance.getValue();
+ }
+ }
+
+ Object convertedValue = convertStringToType(value, variable.getType());
+ if (convertedValue instanceof OptimizelyJSON) {
+ convertedValue = ((OptimizelyJSON) convertedValue).toMap();
+ }
+
+ valuesMap.put(variable.getKey(), convertedValue);
+ }
+
+ DecisionNotification decisionNotification = DecisionNotification.newFeatureVariableDecisionNotificationBuilder()
+ .withUserId(userId)
+ .withAttributes(copiedAttributes)
+ .withFeatureKey(featureKey)
+ .withFeatureEnabled(featureEnabled)
+ .withVariableValues(valuesMap)
+ .withFeatureDecision(featureDecision)
+ .build();
+
+ notificationCenter.send(decisionNotification);
+
+ return new OptimizelyJSON(valuesMap);
+ }
+
/**
* Get the list of features that are enabled for the user.
* TODO revisit this method. Calling this as-is can dramatically increase visitor impression counts.
@@ -732,7 +1039,7 @@ Object convertStringToType(String variableValue, String type) {
* return Empty List.
*/
public List getEnabledFeatures(@Nonnull String userId, @Nonnull Map attributes) {
- List enabledFeaturesList = new ArrayList();
+ List enabledFeaturesList = new ArrayList();
if (!validateUserId(userId)) {
return enabledFeaturesList;
}
@@ -759,7 +1066,7 @@ public List getEnabledFeatures(@Nonnull String userId, @Nonnull MapemptyMap());
+ return getVariation(experiment, userId, Collections.emptyMap());
}
@Nullable
@@ -775,8 +1082,7 @@ private Variation getVariation(@Nonnull ProjectConfig projectConfig,
@Nonnull String userId,
@Nonnull Map attributes) throws UnknownExperimentException {
Map copiedAttributes = copyAttributes(attributes);
- Variation variation = decisionService.getVariation(experiment, userId, copiedAttributes, projectConfig);
-
+ Variation variation = decisionService.getVariation(experiment, createUserContextCopy(userId, copiedAttributes), projectConfig).getResult();
String notificationType = NotificationCenter.DecisionNotificationType.AB_TEST.toString();
if (projectConfig.getExperimentFeatureKeyMapping().get(experiment.getId()) != null) {
@@ -889,7 +1195,7 @@ public Variation getForcedVariation(@Nonnull String experimentKey,
return null;
}
- return decisionService.getForcedVariation(experiment, userId);
+ return decisionService.getForcedVariation(experiment, userId).getResult();
}
/**
@@ -941,6 +1247,273 @@ public OptimizelyConfig getOptimizelyConfig() {
return new OptimizelyConfigService(projectConfig).getConfig();
}
+ //============ decide ============//
+
+ /**
+ * Create a context of the user for which decision APIs will be called.
+ *
+ * A user context will be created successfully even when the SDK is not fully configured yet.
+ *
+ * @param userId The user ID to be used for bucketing.
+ * @param attributes: A map of attribute names to current user attribute values.
+ * @return An OptimizelyUserContext associated with this OptimizelyClient.
+ */
+ public OptimizelyUserContext createUserContext(@Nonnull String userId,
+ @Nonnull Map attributes) {
+ if (userId == null) {
+ logger.warn("The userId parameter must be nonnull.");
+ return null;
+ }
+
+ return new OptimizelyUserContext(this, userId, attributes);
+ }
+
+ public OptimizelyUserContext createUserContext(@Nonnull String userId) {
+ return new OptimizelyUserContext(this, userId);
+ }
+
+ private OptimizelyUserContext createUserContextCopy(@Nonnull String userId, @Nonnull Map attributes) {
+ if (userId == null) {
+ logger.warn("The userId parameter must be nonnull.");
+ return null;
+ }
+ return new OptimizelyUserContext(this, userId, attributes, Collections.EMPTY_MAP, null, false);
+ }
+
+ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
+ @Nonnull String key,
+ @Nonnull List options) {
+ ProjectConfig projectConfig = getProjectConfig();
+ if (projectConfig == null) {
+ return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason());
+ }
+
+ List allOptions = getAllOptions(options);
+ allOptions.remove(OptimizelyDecideOption.ENABLED_FLAGS_ONLY);
+
+ return decideForKeys(user, Arrays.asList(key), allOptions, true).get(key);
+ }
+
+ private OptimizelyDecision createOptimizelyDecision(
+ OptimizelyUserContext user,
+ String flagKey,
+ FeatureDecision flagDecision,
+ DecisionReasons decisionReasons,
+ List allOptions,
+ ProjectConfig projectConfig
+ ) {
+ String userId = user.getUserId();
+ String experimentId = null;
+ String variationId = null;
+
+ Boolean flagEnabled = false;
+ if (flagDecision.variation != null) {
+ if (flagDecision.variation.getFeatureEnabled()) {
+ flagEnabled = true;
+ }
+ }
+ logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", flagKey, userId, flagEnabled);
+
+ Map variableMap = new HashMap<>();
+ if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) {
+ DecisionResponse