├── .editorconfig ├── .github ├── stale.yml └── workflows │ ├── add-to-project.yml │ ├── benchmarks.yml │ ├── main.yml │ ├── publish_javadoc.yml │ ├── pull_requests.yml │ ├── release_changelog.yml │ ├── release_jreleaser.yml │ └── release_to_central.yml ├── .gitignore ├── .gitmodules ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CHANGELOG.md ├── LICENSE ├── README.md ├── benches └── jmh │ └── unleash-client-benches │ ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── dependency-reduced-pom.xml │ ├── mvnw │ ├── mvnw.cmd │ ├── output.json │ ├── pom.xml │ └── src │ └── main │ ├── java │ └── io │ │ └── getunleash │ │ └── UnleashClientBenchmark.java │ └── resources │ └── unleash-repo-v2-with-impression-data.json ├── examples ├── cli-example │ ├── .gitattributes │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ └── src │ │ └── main │ │ ├── java │ │ └── io │ │ │ └── getunleash │ │ │ └── example │ │ │ └── AdvancedConstraints.java │ │ └── resources │ │ └── logback.xml ├── okhttp-example │ ├── build.gradle.kts │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── getunleash │ │ └── example │ │ └── UnleashOkHttp.java └── spring-boot-example │ ├── .gitattributes │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ └── src │ └── main │ ├── java │ └── io │ │ └── getunleash │ │ └── unleash │ │ └── example │ │ ├── ExampleApplication.java │ │ ├── ProxyController.java │ │ └── UnleashSpringConfig.java │ └── resources │ └── application.yaml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src ├── main │ ├── java │ │ └── io │ │ │ └── getunleash │ │ │ ├── CustomHttpHeadersProvider.java │ │ │ ├── DefaultCustomHttpHeadersProviderImpl.java │ │ │ ├── DefaultUnleash.java │ │ │ ├── EngineProxy.java │ │ │ ├── EngineProxyImpl.java │ │ │ ├── EvaluatedToggle.java │ │ │ ├── FakeUnleash.java │ │ │ ├── FeatureDefinition.java │ │ │ ├── MoreOperations.java │ │ │ ├── Unleash.java │ │ │ ├── UnleashContext.java │ │ │ ├── UnleashContextProvider.java │ │ │ ├── UnleashException.java │ │ │ ├── event │ │ │ ├── ClientFeaturesResponse.java │ │ │ ├── EventDispatcher.java │ │ │ ├── FeatureSet.java │ │ │ ├── ImpressionEvent.java │ │ │ ├── IsEnabledImpressionEvent.java │ │ │ ├── Log4JSubscriber.java │ │ │ ├── NoOpSubscriber.java │ │ │ ├── ToggleEvaluated.java │ │ │ ├── UnleashEvent.java │ │ │ ├── UnleashReady.java │ │ │ ├── UnleashSubscriber.java │ │ │ ├── VariantImpressionEvent.java │ │ │ └── package-info.java │ │ │ ├── lang │ │ │ ├── NonNullApi.java │ │ │ ├── NonNullFields.java │ │ │ └── Nullable.java │ │ │ ├── metric │ │ │ ├── ClientMetrics.java │ │ │ ├── ClientRegistration.java │ │ │ ├── DefaultHttpMetricsSender.java │ │ │ ├── MetricSender.java │ │ │ ├── OkHttpMetricsSender.java │ │ │ ├── UnleashMetricService.java │ │ │ ├── UnleashMetricServiceImpl.java │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ ├── repository │ │ │ ├── BackupHandler.java │ │ │ ├── FeatureBackupHandlerFile.java │ │ │ ├── FeatureFetcher.java │ │ │ ├── FeatureRepository.java │ │ │ ├── FeatureRepositoryImpl.java │ │ │ ├── HttpFeatureFetcher.java │ │ │ ├── OkHttpFeatureFetcher.java │ │ │ ├── ToggleBootstrapFileProvider.java │ │ │ ├── ToggleBootstrapProvider.java │ │ │ ├── YggdrasilAdapters.java │ │ │ └── package-info.java │ │ │ ├── strategy │ │ │ ├── Strategy.java │ │ │ └── package-info.java │ │ │ ├── util │ │ │ ├── AtomicLongSerializer.java │ │ │ ├── ClientFeaturesParser.java │ │ │ ├── DateTimeSerializer.java │ │ │ ├── FeatureDefinitionAdapter.java │ │ │ ├── InstantSerializer.java │ │ │ ├── MetricSenderFactory.java │ │ │ ├── OkHttpClientConfigurer.java │ │ │ ├── Throttler.java │ │ │ ├── UnleashConfig.java │ │ │ ├── UnleashFeatureFetcherFactory.java │ │ │ ├── UnleashProperties.java │ │ │ ├── UnleashScheduledExecutor.java │ │ │ ├── UnleashScheduledExecutorImpl.java │ │ │ ├── UnleashURLs.java │ │ │ └── package-info.java │ │ │ └── variant │ │ │ ├── Payload.java │ │ │ ├── Variant.java │ │ │ └── package-info.java │ └── resources │ │ ├── META-INF │ │ └── native-image │ │ │ └── io.getunleash │ │ │ └── unleash-client-java │ │ │ ├── reflect-config.json │ │ │ └── resource-config.json │ │ └── app.properties └── test │ ├── java │ └── io │ │ └── getunleash │ │ ├── DefaultUnleashTest.java │ │ ├── FakeUnleashTest.java │ │ ├── RunOnJavaVersions.java │ │ ├── RunOnJavaVersionsCondition.java │ │ ├── SynchronousTestExecutor.java │ │ ├── UnleashContextTest.java │ │ ├── UnleashTest.java │ │ ├── event │ │ ├── ImpressionDataSubscriberTest.java │ │ └── SubscriberTest.java │ │ ├── example │ │ ├── CustomStrategy.java │ │ └── UnleashUsageTest.java │ │ ├── integration │ │ ├── ClientSpecificationTest.java │ │ ├── TestCase.java │ │ ├── TestCaseVariant.java │ │ ├── TestDefinition.java │ │ └── UnleashContextDefinition.java │ │ ├── metric │ │ ├── DefaultHttpMetricsSenderTest.java │ │ └── UnleashMetricServiceImplTest.java │ │ ├── repository │ │ ├── FeatureBackupHandlerFileTest.java │ │ ├── FeatureRepositoryTest.java │ │ ├── HttpFeatureFetcherTest.java │ │ ├── OkHttpFeatureFetcherTest.java │ │ ├── ToggleBootstrapFileProviderTest.java │ │ └── UnleashExceptionExtension.java │ │ └── util │ │ ├── ClientFeaturesParserTest.java │ │ ├── ThrottlerTest.java │ │ ├── UnleashConfigTest.java │ │ ├── UnleashScheduledExecutorImplTest.java │ │ └── UnleashURLsTest.java │ └── resources │ ├── __files │ ├── features-v0.json │ ├── features-v1-with-variants.json │ ├── features-v1.json │ └── features-v2-with-segments.json │ ├── empty-v1.json │ ├── empty.json │ ├── features-v0.json │ ├── features-v1-empty.json │ ├── features-v1-with-variants.json │ ├── features-v1.json │ ├── features-v2-empty.json │ ├── features-v2-with-segments.json │ ├── logback-test.xml │ ├── unleash-repo-v0.json │ ├── unleash-repo-v1.json │ ├── unleash-repo-v2-advanced.json │ ├── unleash-repo-v2-with-impression-data.json │ ├── unleash-repo-v2.json │ └── unleash-repo-without-feature-field.json └── v10_MIGRATION_GUIDE.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.yml] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add new item to project board 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | pull_request_target: 8 | types: 9 | - opened 10 | 11 | jobs: 12 | add-to-project: 13 | uses: unleash/.github/.github/workflows/add-item-to-project.yml@main 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Run benchmarks 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | 15 | jobs: 16 | jmh_benchmark: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Setup JDK 17 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: temurin 24 | java-version: 17 25 | - name: Build client 26 | run: | 27 | mvn clean install -DskipTests 28 | - name: Get project version 29 | id: unleash 30 | run: | 31 | VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) 32 | echo "::set-output name=version::$VERSION" 33 | - name: Build and run benchmark 34 | run: | 35 | cd ./benches/jmh/unleash-client-benches 36 | mvn clean install -Dunleash.version=${{ steps.unleash.outputs.version }} 37 | java -jar target/benchmarks.jar -w1 -rf json -rff output.json 38 | - name: JMH Benchmark Action 39 | uses: kitlangton/jmh-benchmark-action@main 40 | with: 41 | jmh-output-path: benches/jmh/unleash-client-benches/output.json 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | version: [8, 11, 17] 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Setup Java 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: ${{ matrix.version }} 21 | cache: "maven" 22 | distribution: "temurin" 23 | - name: Build, test, coverage 24 | run: ./mvnw clean test jacoco:report 25 | - name: Coveralls 26 | uses: coverallsapp/github-action@v2 27 | with: 28 | github-token: ${{ secrets.GITHUB_TOKEN }} 29 | allow-empty: true 30 | base-path: src/main/java 31 | parallel: true 32 | flag-name: run-jvm-${{ join(matrix.*, '-') }} 33 | finish: 34 | needs: build 35 | if: ${{ always() }} 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 42 | - name: Coveralls Finished 43 | uses: coverallsapp/github-action@v2 44 | with: 45 | parallel-finished: true 46 | carryforward: run-jvm-8,run-jvm-11,run-jvm-17 47 | 48 | -------------------------------------------------------------------------------- /.github/workflows/publish_javadoc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Javadocs 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | javadoc: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Setup Java 16 | uses: actions/setup-java@v4 17 | with: 18 | java-version: 17 19 | distribution: 'temurin' 20 | - name: Build 21 | run: ./mvnw javadoc:javadoc 22 | - name: Deploy docs to pages 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ./target/site/apidocs 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/pull_requests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | 4 | jobs: 5 | build: 6 | runs-on: ${{ matrix.os }}-latest 7 | strategy: 8 | matrix: 9 | version: [8, 11, 17] 10 | os: ["ubuntu", "windows", "macos"] 11 | exclude: 12 | - version: 8 13 | os: macos 14 | - version: 8 15 | os: windows 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Setup Java 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: ${{ matrix.version }} 23 | cache: "maven" 24 | distribution: "temurin" 25 | - name: Build, test, coverage 26 | run: ./mvnw clean test jacoco:report 27 | - name: Coveralls parallel 28 | uses: coverallsapp/github-action@v2 29 | with: 30 | github-token: ${{ secrets.GITHUB_TOKEN }} 31 | allow-empty: true 32 | flag-name: run-jvm-${{ join(matrix.*, '-') }} 33 | parallel: true 34 | base-path: src/main/java 35 | finish: 36 | needs: build 37 | if: ${{ always() }} 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | with: 43 | fetch-depth: 0 44 | 45 | - name: Coveralls Finished 46 | uses: coverallsapp/github-action@v2 47 | with: 48 | parallel-finished: true 49 | carryforward: run-jvm-8,run-jvm-11,run-jvm-17 50 | -------------------------------------------------------------------------------- /.github/workflows/release_changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Releases" 2 | on: 3 | push: 4 | tags: 5 | - "unleash-client-java-*" 6 | 7 | jobs: 8 | release: 9 | if: startsWith(github.ref, 'refs/tags/') 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Build changelog 15 | id: github_release 16 | uses: metcalfc/changelog-generator@v4.2.0 17 | with: 18 | myToken: ${{ secrets.GITHUB_TOKEN }} 19 | - name: Create release 20 | uses: ncipollo/release-action@v1 21 | with: 22 | body: ${{ steps.github_release.outputs.changelog }} 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN}} 25 | finish: 26 | needs: build 27 | if: ${{ always() }} 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Coveralls Finished 31 | uses: coverallsapp/github-action@v2 32 | with: 33 | parallel-finished: true 34 | carryforward: run-jvm-8,run-jvm-11,run-jvm-17 35 | -------------------------------------------------------------------------------- /.github/workflows/release_jreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Release (using JReleaser) 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Release version" 8 | required: true 9 | nextVersion: 10 | description: "Next version after release (-SNAPSHOT will be added automatically)" 11 | required: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 45 17 | steps: 18 | - name: Generate token 19 | id: generate-token 20 | uses: actions/create-github-app-token@v1 21 | with: 22 | app-id: ${{ secrets.UNLEASH_BOT_APP_ID }} 23 | private-key: ${{ secrets.UNLEASH_BOT_PRIVATE_KEY }} 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | token: ${{ steps.generate-token.outputs.token }} 29 | - name: Set up JDK 17 30 | uses: actions/setup-java@v4 31 | with: 32 | java-version: "17" 33 | distribution: "temurin" 34 | cache: maven 35 | - name: Set release version 36 | run: | 37 | mvn --no-transfer-progress --batch-mode versions:set -DnewVersion=${{ github.event.inputs.version }} 38 | - name: Commit & Push changes 39 | uses: actions-js/push@master 40 | with: 41 | github_token: ${{ steps.generate-token.outputs.token }} 42 | message: Releasing version ${{ github.event.inputs.version }} 43 | - name: Stage release 44 | run: | 45 | mvn --no-transfer-progress --batch-mode -Ppublication clean deploy 46 | - name: Run JReleaser 47 | run: | 48 | mvn jreleaser:full-release 49 | env: 50 | JRELEASER_PROJECT_VERSION: ${{ github.event.inputs.version }} 51 | JRELEASER_GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} 52 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 53 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} 54 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} 55 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }} 56 | JRELEASER_MAVENCENTRAL_TOKEN: ${{ secrets.JRELEASER_MAVENCENTRAL_TOKEN }} 57 | - name: Set next version 58 | run: | 59 | mvn --no-transfer-progress --batch-mode versions:set -DnewVersion=${{ github.event.inputs.nextVersion }}-SNAPSHOT 60 | - name: Commit & Push changes 61 | uses: actions-js/push@master 62 | with: 63 | github_token: ${{ steps.generate-token.outputs.token }} 64 | message: Setting SNAPSHOT version ${{ github.event.inputs.nextVersion }}-SNAPSHOT 65 | - name: JReleaser release output 66 | if: always() 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: jreleaser-release 70 | path: | 71 | out/jreleaser/trace.log 72 | out/jreleaser/output.properties 73 | -------------------------------------------------------------------------------- /.github/workflows/release_to_central.yml: -------------------------------------------------------------------------------- 1 | name: Release to central 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: What version would you like to use? 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | name: Checkout code 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup Java and Maven Central Repo 23 | uses: actions/setup-java@v4 24 | with: 25 | java-version: "8" 26 | distribution: "temurin" 27 | server-id: ossrh 28 | server-username: MAVEN_USERNAME 29 | server-password: MAVEN_PASSWORD 30 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 31 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 32 | cache: maven 33 | 34 | - name: Setup git config 35 | run: | 36 | git config user.name "Github Release Bot" 37 | git config user.email "<>" 38 | - name: Release 39 | run: mvn -B -DreleaseVersion=${{ inputs.version}} release:prepare release:perform 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 43 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 44 | MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Idea stuff 31 | .idea 32 | *.iml 33 | 34 | # Eclipse stuff 35 | .project 36 | .settings 37 | .classpath 38 | 39 | # Java 40 | target 41 | 42 | # webpack output 43 | /unleash-server/public/js/bundle.js 44 | 45 | .DS_Store 46 | **/bin/ 47 | .vim/ 48 | *.versionsBackup 49 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-client-java/fb576bfd7d68cfd27d4a32109a9735a197659f5b/.gitmodules -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.5"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-client-java/fb576bfd7d68cfd27d4a32109a9735a197659f5b/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar 3 | -------------------------------------------------------------------------------- /benches/jmh/unleash-client-benches/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-client-java/fb576bfd7d68cfd27d4a32109a9735a197659f5b/benches/jmh/unleash-client-benches/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /benches/jmh/unleash-client-benches/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /benches/jmh/unleash-client-benches/dependency-reduced-pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | io.getunleash 5 | unleash-client-benches 6 | JMH benchmark sample: Java 7 | 1.0 8 | 9 | 10 | 11 | 12 | maven-clean-plugin 13 | 3.2.0 14 | 15 | 16 | maven-deploy-plugin 17 | 3.1.2 18 | 19 | 20 | maven-install-plugin 21 | 3.1.2 22 | 23 | 24 | maven-jar-plugin 25 | 3.4.2 26 | 27 | 28 | maven-javadoc-plugin 29 | 3.11.2 30 | 31 | 32 | maven-resources-plugin 33 | 3.3.1 34 | 35 | 36 | maven-site-plugin 37 | 3.12.1 38 | 39 | 40 | maven-source-plugin 41 | 3.3.1 42 | 43 | 44 | maven-surefire-plugin 45 | 3.5.2 46 | 47 | 48 | 49 | 50 | 51 | maven-compiler-plugin 52 | 3.13.0 53 | 54 | ${javac.target} 55 | ${javac.target} 56 | ${javac.target} 57 | 58 | 59 | org.openjdk.jmh 60 | jmh-generator-annprocess 61 | ${jmh.version} 62 | 63 | 64 | 65 | 66 | 67 | maven-shade-plugin 68 | 3.6.0 69 | 70 | 71 | package 72 | 73 | shade 74 | 75 | 76 | ${uberjar.name} 77 | 78 | 79 | org.openjdk.jmh.Main 80 | 81 | 82 | 83 | 84 | 85 | *:* 86 | 87 | META-INF/*.SF 88 | META-INF/*.DSA 89 | META-INF/*.RSA 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.openjdk.jmh 102 | jmh-generator-annprocess 103 | 1.37 104 | provided 105 | 106 | 107 | 108 | 1.37 109 | UTF-8 110 | 1.8 111 | benchmarks 112 | 113 | 114 | -------------------------------------------------------------------------------- /benches/jmh/unleash-client-benches/src/main/java/io/getunleash/UnleashClientBenchmark.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import io.getunleash.repository.ToggleBootstrapFileProvider; 4 | import io.getunleash.util.UnleashConfig; 5 | import org.openjdk.jmh.annotations.*; 6 | import org.openjdk.jmh.infra.Blackhole; 7 | import org.openjdk.jmh.runner.Runner; 8 | import org.openjdk.jmh.runner.RunnerException; 9 | import org.openjdk.jmh.runner.options.Options; 10 | import org.openjdk.jmh.runner.options.OptionsBuilder; 11 | 12 | import java.util.concurrent.TimeUnit; 13 | 14 | @State(Scope.Benchmark) 15 | @Fork(value = 1) 16 | @Warmup(iterations = 3, timeUnit = TimeUnit.MILLISECONDS, time = 2000) 17 | @Measurement(iterations = 5, timeUnit = TimeUnit.MILLISECONDS, time = 5000) 18 | public class UnleashClientBenchmark { 19 | 20 | @State(Scope.Benchmark) 21 | public static class MyState { 22 | 23 | public Unleash unleash; 24 | public UnleashContext context; 25 | 26 | @Setup(Level.Trial) 27 | public void doSetup() { 28 | System.out.println("dosetup"); 29 | unleash = new DefaultUnleash(UnleashConfig.builder().unleashAPI("https://localhost:1500") 30 | .apiKey("irrelevant").appName("UnleashBenchmarks") 31 | .toggleBootstrapProvider( 32 | new ToggleBootstrapFileProvider("classpath:./unleash-repo-v2-with-impression-data.json")) 33 | .fetchTogglesInterval(0).disablePolling().disableMetrics().build()); 34 | context = new UnleashContext.Builder().environment("benchmarking").build(); 35 | } 36 | 37 | @TearDown(Level.Trial) 38 | public void doTearDown() { 39 | System.out.println("Do TearDown"); 40 | } 41 | 42 | } 43 | 44 | public static void main(String[] args) throws RunnerException { 45 | Options opt = new OptionsBuilder().include(UnleashClientBenchmark.class.getSimpleName()).forks(1).build(); 46 | new Runner(opt).run(); 47 | } 48 | 49 | @Benchmark 50 | public void isEnabled(MyState myState, Blackhole bh) { 51 | bh.consume(myState.unleash.isEnabled("Test.impressionDataPresent")); 52 | } 53 | 54 | @Benchmark 55 | public void isEnabledWithContext(MyState myState, Blackhole bh) { 56 | bh.consume(myState.unleash.isEnabled("Test.impressionDataPresent", myState.context)); 57 | } 58 | 59 | @Benchmark 60 | public void getDefaultVariant(MyState myState, Blackhole bh) { 61 | bh.consume(myState.unleash.getVariant("Test.impressionDataPresent")); 62 | } 63 | 64 | @Benchmark 65 | public void getVariant(MyState myState, Blackhole bh) { 66 | bh.consume(myState.unleash.getVariant("Test.variants")); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /benches/jmh/unleash-client-benches/src/main/resources/unleash-repo-v2-with-impression-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "features": [ 4 | { 5 | "name": "Test.impressionDataPresent", 6 | "description": "It's true, we do have impression data", 7 | "enabled": true, 8 | "strategies": [ 9 | { 10 | "name": "default" 11 | } 12 | ], 13 | "impressionData": true, 14 | "variants": null, 15 | "createdAt": "2019-01-24T10:38:10.370Z" 16 | }, 17 | { 18 | "name": "Test.variants", 19 | "description": null, 20 | "enabled": true, 21 | "strategies": [ 22 | { 23 | "name": "default", 24 | "segments": [ 25 | 1 26 | ] 27 | } 28 | ], 29 | "variants": [ 30 | { 31 | "name": "variant1", 32 | "weight": 50 33 | }, 34 | { 35 | "name": "variant2", 36 | "weight": 50 37 | } 38 | ], 39 | "createdAt": "2019-01-24T10:41:45.236Z" 40 | }, 41 | { 42 | "name": "Test.impressionDataSetToOff", 43 | "description": "Explicitly no impression data", 44 | "enabled": true, 45 | "strategies": [ 46 | { 47 | "name": "default" 48 | } 49 | ], 50 | "impressionData": false, 51 | "variants": null, 52 | "createdAt": "2019-01-24T10:38:10.370Z" 53 | }, 54 | { 55 | "name": "Test.impressionDataMissing", 56 | "description": "No field defined", 57 | "enabled": true, 58 | "strategies": [ 59 | { 60 | "name": "default" 61 | } 62 | ], 63 | "variants": null, 64 | "createdAt": "2019-01-24T10:38:10.370Z" 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /examples/cli-example/.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /examples/cli-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /examples/cli-example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | application 4 | } 5 | 6 | application { 7 | mainClass.set("io.getunleash.example.AdvancedConstraints") 8 | } 9 | 10 | repositories { 11 | mavenCentral() 12 | mavenLocal() 13 | } 14 | 15 | dependencies { 16 | implementation("io.getunleash:unleash-client-java:10.2.0") 17 | implementation("ch.qos.logback:logback-classic:1.4.12") 18 | } 19 | -------------------------------------------------------------------------------- /examples/cli-example/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-client-java/fb576bfd7d68cfd27d4a32109a9735a197659f5b/examples/cli-example/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/cli-example/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /examples/cli-example/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /examples/cli-example/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/7.4.1/userguide/multi_project_builds.html 8 | * This project uses @Incubating APIs which are subject to change. 9 | */ 10 | 11 | rootProject.name = "cli-example" 12 | -------------------------------------------------------------------------------- /examples/cli-example/src/main/java/io/getunleash/example/AdvancedConstraints.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.example; 2 | 3 | import io.getunleash.DefaultUnleash; 4 | import io.getunleash.Unleash; 5 | import io.getunleash.UnleashContext; 6 | import io.getunleash.util.UnleashConfig; 7 | 8 | public class AdvancedConstraints { 9 | 10 | public static void main(String[] args) throws InterruptedException { 11 | UnleashConfig config = UnleashConfig.builder() 12 | .appName("client-example.advanced.java") 13 | .customHttpHeader( 14 | "Authorization", 15 | getOrElse("UNLEASH_API_TOKEN", 16 | "*:development.25a06b75248528f8ca93ce179dcdd141aedfb632231e0d21fd8ff349")) 17 | .unleashAPI(getOrElse("UNLEASH_API_URL", "https://app.unleash-hosted.com/demo/api")) 18 | .instanceId("java-example") 19 | .synchronousFetchOnInitialisation(true) 20 | .sendMetricsInterval(30).build(); 21 | 22 | Unleash unleash = new DefaultUnleash(config); 23 | UnleashContext context = UnleashContext.builder() 24 | .addProperty("semver", "1.5.2") 25 | .build(); 26 | UnleashContext smallerSemver = UnleashContext.builder() 27 | .addProperty("semver", "1.1.0") 28 | .build(); 29 | while (true) { 30 | unleash.isEnabled("advanced.constraints", context); // expect this to be true 31 | unleash.isEnabled("advanced.constraints", smallerSemver); // expect this to be false 32 | } 33 | } 34 | 35 | public static String getOrElse(String key, String defaultValue) { 36 | String value = System.getenv(key); 37 | if (value == null) { 38 | return defaultValue; 39 | } 40 | return value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/cli-example/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/okhttp-example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | application 4 | } 5 | 6 | application { 7 | mainClass.set("io.getunleash.example.UnleashOkHttp") 8 | } 9 | repositories { 10 | mavenCentral() 11 | mavenLocal() 12 | } 13 | 14 | dependencies { 15 | implementation("io.getunleash:unleash-client-java:8.2.0") 16 | implementation("com.squareup.okhttp3:okhttp:4.9.3") 17 | } 18 | -------------------------------------------------------------------------------- /examples/okhttp-example/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-client-java/fb576bfd7d68cfd27d4a32109a9735a197659f5b/examples/okhttp-example/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/okhttp-example/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /examples/okhttp-example/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /examples/okhttp-example/src/main/java/io/getunleash/example/UnleashOkHttp.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.example; 2 | 3 | import io.getunleash.DefaultUnleash; 4 | import io.getunleash.Unleash; 5 | import io.getunleash.UnleashContext; 6 | import io.getunleash.UnleashException; 7 | import io.getunleash.event.UnleashReady; 8 | import io.getunleash.event.UnleashSubscriber; 9 | import io.getunleash.repository.FeatureToggleResponse; 10 | import io.getunleash.repository.OkHttpFeatureFetcher; 11 | import io.getunleash.util.UnleashConfig; 12 | 13 | public class UnleashOkHttp { 14 | public static void main(String[] args) throws InterruptedException { 15 | 16 | UnleashConfig config = UnleashConfig.builder().appName("client-example.okhttp") 17 | .customHttpHeader("Authorization", 18 | "*:production.ZvzGdauVXYPyevrQVqnt8LSRHKuW") 19 | .unleashAPI("http://localhost:1500/api").instanceId("okhttp-example") 20 | .unleashFeatureFetcherFactory(OkHttpFeatureFetcher::new) 21 | .fetchTogglesInterval(10) 22 | .subscriber(new UnleashSubscriber() { 23 | @Override 24 | public void onReady(UnleashReady unleashReady) { 25 | System.out.println("Ready"); 26 | } 27 | 28 | @Override 29 | public void togglesFetched(FeatureToggleResponse toggleResponse) { 30 | System.out.println("Fetched toggles. " + toggleResponse); 31 | } 32 | 33 | @Override 34 | public void onError(UnleashException unleashException) { 35 | System.out.println("Failed " + unleashException); 36 | } 37 | }) 38 | .synchronousFetchOnInitialisation(true) 39 | .build(); 40 | Unleash unleash = new DefaultUnleash(config); 41 | unleash.more().getFeatureToggleNames().forEach(t -> System.out.println(t)); 42 | while (true) { 43 | Thread.sleep(5000); 44 | System.out.println(unleash.isEnabled("my.feature", 45 | UnleashContext.builder().addProperty("email", "test@getunleash.ai").build())); 46 | System.out.println(unleash.getVariant("my.feature")); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/spring-boot-example/.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /examples/spring-boot-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /examples/spring-boot-example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | id("org.springframework.boot") version "3.2.0" 4 | id("io.spring.dependency-management") version "1.1.4" 5 | } 6 | 7 | repositories { 8 | mavenLocal() 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation("org.springframework.boot:spring-boot-starter-web") 14 | implementation("io.getunleash:unleash-client-java:9.1.1") 15 | testImplementation("org.springframework.boot:spring-boot-starter-test") 16 | } 17 | 18 | tasks.withType { 19 | useJUnitPlatform() 20 | } 21 | -------------------------------------------------------------------------------- /examples/spring-boot-example/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-client-java/fb576bfd7d68cfd27d4a32109a9735a197659f5b/examples/spring-boot-example/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/spring-boot-example/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /examples/spring-boot-example/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /examples/spring-boot-example/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/7.5.1/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = "spring-boot-example" 11 | -------------------------------------------------------------------------------- /examples/spring-boot-example/src/main/java/io/getunleash/unleash/example/ExampleApplication.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.unleash.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | 5 | public class ExampleApplication { 6 | public static void main(String[] args) { 7 | SpringApplication.run(UnleashSpringConfig.class, args); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/spring-boot-example/src/main/java/io/getunleash/unleash/example/ProxyController.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.unleash.example; 2 | 3 | import io.getunleash.Unleash; 4 | import jakarta.websocket.server.PathParam; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | @RestController 14 | public class ProxyController { 15 | private Unleash unleash; 16 | 17 | @Autowired 18 | public ProxyController(Unleash unleash) { 19 | this.unleash = unleash; 20 | } 21 | 22 | 23 | @GetMapping("/") 24 | public Map getEnabledToggles() { 25 | return toggles() 26 | .filter(Map.Entry::getValue) 27 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 28 | } 29 | 30 | @GetMapping("/all") 31 | public Map getAllToggles() { 32 | return toggles().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 33 | } 34 | 35 | @GetMapping("/toggle/{toggleName}") 36 | public Boolean getToggle(@PathParam("toggleName") String name) { 37 | return unleash.isEnabled(name); 38 | } 39 | 40 | private Stream> toggles() { 41 | return unleash.more().getFeatureToggleNames().stream().map(name -> Map.entry(name, unleash.isEnabled(name))); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/spring-boot-example/src/main/java/io/getunleash/unleash/example/UnleashSpringConfig.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.unleash.example; 2 | 3 | import io.getunleash.DefaultUnleash; 4 | import io.getunleash.Unleash; 5 | import io.getunleash.util.UnleashConfig; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | @SpringBootApplication 11 | public class UnleashSpringConfig { 12 | 13 | @Bean 14 | public UnleashConfig unleashConfig(@Value("${unleash.url}") String url, @Value("${unleash.apikey}") String apiKey, 15 | @Value("${unleash.appname}") String appName) { 16 | UnleashConfig config = UnleashConfig.builder().unleashAPI(url).apiKey(apiKey).appName(appName) 17 | .synchronousFetchOnInitialisation(true) 18 | .fetchTogglesInterval(15).build(); 19 | return config; 20 | } 21 | 22 | @Bean 23 | public Unleash unleash(UnleashConfig unleashConfig) { 24 | return new DefaultUnleash(unleashConfig); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/spring-boot-example/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | unleash: 2 | url: https://app.unleash-hosted.com/demo/api 3 | apiKey: demo-app:production.614a75cf68bef8703aa1bd8304938a81ec871f86ea40c975468eabd6 4 | appname: "Java-Spring-Boot-example" 5 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/CustomHttpHeadersProvider.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import java.util.Map; 4 | 5 | public interface CustomHttpHeadersProvider { 6 | Map getCustomHeaders(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/DefaultCustomHttpHeadersProviderImpl.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class DefaultCustomHttpHeadersProviderImpl implements CustomHttpHeadersProvider { 7 | @Override 8 | public Map getCustomHeaders() { 9 | return new HashMap<>(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/EngineProxy.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import io.getunleash.metric.UnleashMetricService; 4 | import io.getunleash.repository.FeatureRepository; 5 | 6 | public interface EngineProxy extends FeatureRepository, UnleashMetricService {} 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/EngineProxyImpl.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import io.getunleash.engine.UnleashEngine; 4 | import io.getunleash.engine.VariantDef; 5 | import io.getunleash.lang.Nullable; 6 | import io.getunleash.metric.UnleashMetricService; 7 | import io.getunleash.metric.UnleashMetricServiceImpl; 8 | import io.getunleash.repository.FeatureRepositoryImpl; 9 | import io.getunleash.repository.YggdrasilAdapters; 10 | import io.getunleash.strategy.Strategy; 11 | import io.getunleash.util.UnleashConfig; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.Optional; 15 | import java.util.Set; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.Stream; 18 | 19 | public class EngineProxyImpl implements EngineProxy { 20 | 21 | UnleashEngine unleashEngine; 22 | FeatureRepositoryImpl featureRepository; 23 | UnleashMetricService metricService; 24 | 25 | public EngineProxyImpl(UnleashConfig unleashConfig, Strategy... strategies) { 26 | Map strategyMap = buildStrategyMap(strategies); 27 | 28 | this.unleashEngine = 29 | new UnleashEngine( 30 | strategyMap.values().stream() 31 | .map(YggdrasilAdapters::adapt) 32 | .collect(Collectors.toList()), 33 | Optional.ofNullable(unleashConfig.getFallbackStrategy()) 34 | .map(YggdrasilAdapters::adapt) 35 | .orElse(null)); 36 | 37 | this.featureRepository = new FeatureRepositoryImpl(unleashConfig, unleashEngine); 38 | this.metricService = 39 | new UnleashMetricServiceImpl( 40 | unleashConfig, unleashConfig.getScheduledExecutor(), this.unleashEngine); 41 | 42 | metricService.register(strategyMap.keySet()); 43 | } 44 | 45 | @Override 46 | public Boolean isEnabled(String toggleName, UnleashContext context) { 47 | return this.featureRepository.isEnabled(toggleName, context); 48 | } 49 | 50 | @Override 51 | public Optional getVariant(String toggleName, UnleashContext context) { 52 | return this.featureRepository.getVariant(toggleName, context); 53 | } 54 | 55 | @Override 56 | public void register(Set strategies) { 57 | this.metricService.register(strategies); 58 | } 59 | 60 | @Override 61 | public void countToggle(String name, boolean enabled) { 62 | this.metricService.countToggle(name, enabled); 63 | } 64 | 65 | @Override 66 | public void countVariant(String name, String variantName) { 67 | this.metricService.countVariant(name, variantName); 68 | } 69 | 70 | @Override 71 | public Stream listKnownToggles() { 72 | return this.featureRepository.listKnownToggles(); 73 | } 74 | 75 | @Override 76 | public boolean shouldEmitImpressionEvent(String toggleName) { 77 | return this.featureRepository.shouldEmitImpressionEvent(toggleName); 78 | } 79 | 80 | private static Map buildStrategyMap(@Nullable Strategy[] strategies) { 81 | Map map = new HashMap<>(); 82 | 83 | if (strategies != null) { 84 | for (Strategy strategy : strategies) { 85 | map.put(strategy.getName(), strategy); 86 | } 87 | } 88 | 89 | return map; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/EvaluatedToggle.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import io.getunleash.lang.Nullable; 4 | import io.getunleash.variant.Variant; 5 | 6 | public class EvaluatedToggle { 7 | private final boolean enabled; 8 | private final String name; 9 | @Nullable private final Variant variant; 10 | 11 | public EvaluatedToggle(String name, boolean enabled, @Nullable Variant variant) { 12 | this.enabled = enabled; 13 | this.name = name; 14 | this.variant = variant; 15 | } 16 | 17 | public boolean isEnabled() { 18 | return enabled; 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | @Nullable 26 | public Variant getVariant() { 27 | return variant; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/FeatureDefinition.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import io.getunleash.engine.FeatureDef; 4 | import java.util.Optional; 5 | 6 | public class FeatureDefinition { 7 | 8 | private final String name; 9 | private final Optional type; 10 | private final String project; 11 | private final boolean environmentEnabled; 12 | 13 | public FeatureDefinition(FeatureDef source) { 14 | this.name = source.getName(); 15 | this.type = source.getType(); 16 | this.project = source.getProject(); 17 | this.environmentEnabled = source.isEnabled(); 18 | } 19 | 20 | public FeatureDefinition( 21 | String name, Optional type, String project, boolean environmentEnabled) { 22 | this.name = name; 23 | this.type = type; 24 | this.project = project; 25 | this.environmentEnabled = environmentEnabled; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public Optional getType() { 33 | return type; 34 | } 35 | 36 | public String getProject() { 37 | return project; 38 | } 39 | 40 | public boolean environmentEnabled() { 41 | return environmentEnabled; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/MoreOperations.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface MoreOperations { 7 | 8 | List getFeatureToggleNames(); 9 | 10 | Optional getFeatureToggleDefinition(String toggleName); 11 | 12 | List evaluateAllToggles(); 13 | 14 | /** 15 | * Evaluate all toggles using the provided context. This does not record the corresponding usage 16 | * metrics for each toggle 17 | * 18 | * @param context 19 | * @return 20 | */ 21 | List evaluateAllToggles(UnleashContext context); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/Unleash.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import io.getunleash.variant.Variant; 4 | import java.util.function.BiPredicate; 5 | 6 | public interface Unleash { 7 | default boolean isEnabled(String toggleName) { 8 | return isEnabled(toggleName, false); 9 | } 10 | 11 | default boolean isEnabled(String toggleName, boolean defaultSetting) { 12 | return isEnabled(toggleName, UnleashContext.builder().build(), defaultSetting); 13 | } 14 | 15 | default boolean isEnabled(String toggleName, UnleashContext context) { 16 | return isEnabled(toggleName, context, false); 17 | } 18 | 19 | default boolean isEnabled(String toggleName, UnleashContext context, boolean defaultSetting) { 20 | return isEnabled(toggleName, context, (n, c) -> defaultSetting); 21 | } 22 | 23 | default boolean isEnabled( 24 | String toggleName, BiPredicate fallbackAction) { 25 | return isEnabled(toggleName, UnleashContext.builder().build(), fallbackAction); 26 | } 27 | 28 | boolean isEnabled( 29 | String toggleName, 30 | UnleashContext context, 31 | BiPredicate fallbackAction); 32 | 33 | Variant getVariant(final String toggleName, final UnleashContext context); 34 | 35 | Variant getVariant( 36 | final String toggleName, final UnleashContext context, final Variant defaultValue); 37 | 38 | default Variant getVariant(final String toggleName) { 39 | return getVariant(toggleName, UnleashContext.builder().build()); 40 | } 41 | 42 | default Variant getVariant(final String toggleName, final Variant defaultValue) { 43 | return getVariant(toggleName, UnleashContext.builder().build(), defaultValue); 44 | } 45 | 46 | default void shutdown() {} 47 | 48 | MoreOperations more(); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/UnleashContextProvider.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | public interface UnleashContextProvider { 4 | UnleashContext getContext(); 5 | 6 | static UnleashContextProvider getDefaultProvider() { 7 | return () -> UnleashContext.builder().build(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/UnleashException.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import io.getunleash.event.UnleashEvent; 4 | import io.getunleash.event.UnleashSubscriber; 5 | import io.getunleash.lang.Nullable; 6 | 7 | public class UnleashException extends RuntimeException implements UnleashEvent { 8 | 9 | public UnleashException(String message, @Nullable Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | @Override 14 | public void publishTo(UnleashSubscriber unleashSubscriber) { 15 | unleashSubscriber.onError(this); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/ClientFeaturesResponse.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import io.getunleash.FeatureDefinition; 4 | import io.getunleash.util.ClientFeaturesParser; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public final class ClientFeaturesResponse implements UnleashEvent { 9 | public enum Status { 10 | NOT_CHANGED, 11 | CHANGED, 12 | UNAVAILABLE, 13 | } 14 | 15 | private final Optional clientFeatures; 16 | private final Status statusCode; 17 | private final int httpStatusCode; 18 | private final Optional location; 19 | private List features; 20 | 21 | private ClientFeaturesResponse( 22 | Status status, 23 | int httpStatusCode, 24 | Optional clientFeatures, 25 | Optional location) { 26 | this.statusCode = status; 27 | this.clientFeatures = clientFeatures; 28 | this.httpStatusCode = httpStatusCode; 29 | this.location = location; 30 | } 31 | 32 | public static ClientFeaturesResponse notChanged() { 33 | return new ClientFeaturesResponse( 34 | Status.NOT_CHANGED, 304, Optional.empty(), Optional.empty()); 35 | } 36 | 37 | public static ClientFeaturesResponse updated(String clientFeatures) { 38 | return new ClientFeaturesResponse( 39 | Status.CHANGED, 200, Optional.of(clientFeatures), Optional.empty()); 40 | } 41 | 42 | public static ClientFeaturesResponse unavailable(int statusCode, Optional location) { 43 | return new ClientFeaturesResponse( 44 | Status.UNAVAILABLE, statusCode, Optional.empty(), location); 45 | } 46 | 47 | public Optional getClientFeatures() { 48 | return clientFeatures; 49 | } 50 | 51 | public int getHttpStatusCode() { 52 | return httpStatusCode; 53 | } 54 | 55 | public Status getStatus() { 56 | return statusCode; 57 | } 58 | 59 | public List getFeatures() { 60 | if (clientFeatures.isPresent() && features == null) { 61 | features = ClientFeaturesParser.parse(clientFeatures.get()); 62 | } 63 | return features; 64 | } 65 | 66 | public String getLocation() { 67 | return location.orElse(null); 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "ClientFeatureResponse:" 73 | + " status=" 74 | + this.getStatus() 75 | + " httpStatus=" 76 | + this.getHttpStatusCode(); 77 | } 78 | 79 | @Override 80 | public void publishTo(UnleashSubscriber unleashSubscriber) { 81 | unleashSubscriber.togglesFetched(this); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/EventDispatcher.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import io.getunleash.util.UnleashConfig; 4 | import io.getunleash.util.UnleashScheduledExecutor; 5 | 6 | public class EventDispatcher { 7 | 8 | private final UnleashSubscriber unleashSubscriber; 9 | private final UnleashScheduledExecutor unleashScheduledExecutor; 10 | 11 | public EventDispatcher(UnleashConfig unleashConfig) { 12 | this.unleashSubscriber = unleashConfig.getSubscriber(); 13 | this.unleashScheduledExecutor = unleashConfig.getScheduledExecutor(); 14 | } 15 | 16 | public void dispatch(UnleashEvent unleashEvent) { 17 | unleashScheduledExecutor.scheduleOnce( 18 | () -> { 19 | unleashSubscriber.on(unleashEvent); 20 | unleashEvent.publishTo(unleashSubscriber); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/FeatureSet.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import io.getunleash.FeatureDefinition; 4 | import io.getunleash.util.ClientFeaturesParser; 5 | import java.util.List; 6 | 7 | public class FeatureSet implements UnleashEvent { 8 | 9 | private final String clientFeatures; 10 | private List features; 11 | 12 | public FeatureSet(String clientFeatures) { 13 | this.clientFeatures = clientFeatures; 14 | } 15 | 16 | public List getFeatures() { 17 | if (features == null) { 18 | features = ClientFeaturesParser.parse(clientFeatures); 19 | } 20 | return features; 21 | } 22 | 23 | @Override 24 | public void publishTo(UnleashSubscriber unleashSubscriber) { 25 | unleashSubscriber.on(this); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/ImpressionEvent.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import io.getunleash.UnleashContext; 4 | import java.util.UUID; 5 | 6 | public class ImpressionEvent implements UnleashEvent { 7 | private String featureName; 8 | 9 | private String eventId; 10 | private boolean enabled; 11 | private UnleashContext context; 12 | 13 | public String getFeatureName() { 14 | return featureName; 15 | } 16 | 17 | public String getEventId() { 18 | return eventId; 19 | } 20 | 21 | public boolean isEnabled() { 22 | return enabled; 23 | } 24 | 25 | public UnleashContext getContext() { 26 | return context; 27 | } 28 | 29 | public ImpressionEvent(String featureName, boolean enabled, UnleashContext context) { 30 | this.featureName = featureName; 31 | this.enabled = enabled; 32 | this.eventId = UUID.randomUUID().toString(); 33 | this.context = context; 34 | } 35 | 36 | ImpressionEvent(String featureName, String eventId, boolean enabled, UnleashContext context) { 37 | this.featureName = featureName; 38 | this.eventId = eventId; 39 | this.enabled = enabled; 40 | this.context = context; 41 | } 42 | 43 | @Override 44 | public void publishTo(UnleashSubscriber unleashSubscriber) { 45 | unleashSubscriber.impression(this); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/IsEnabledImpressionEvent.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import io.getunleash.UnleashContext; 4 | 5 | public class IsEnabledImpressionEvent extends ImpressionEvent { 6 | public IsEnabledImpressionEvent(String featureName, boolean enabled, UnleashContext context) { 7 | super(featureName, enabled, context); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/Log4JSubscriber.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import static org.slf4j.event.Level.*; 4 | 5 | import io.getunleash.UnleashException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.slf4j.event.Level; 9 | 10 | public class Log4JSubscriber implements UnleashSubscriber { 11 | 12 | private static final Logger LOG = LoggerFactory.getLogger(Log4JSubscriber.class); 13 | 14 | private Level eventLevel = INFO; 15 | private Level errorLevel = WARN; 16 | 17 | @Override 18 | public void on(UnleashEvent unleashEvent) { 19 | switch (eventLevel) { 20 | case DEBUG: 21 | LOG.debug(unleashEvent.toString()); 22 | break; 23 | case INFO: 24 | LOG.info(unleashEvent.toString()); 25 | break; 26 | case WARN: 27 | LOG.warn(unleashEvent.toString()); 28 | break; 29 | case ERROR: 30 | LOG.error(unleashEvent.toString()); 31 | break; 32 | case TRACE: 33 | LOG.trace(unleashEvent.toString()); 34 | break; 35 | } 36 | } 37 | 38 | @Override 39 | public void onError(UnleashException unleashException) { 40 | switch (errorLevel) { 41 | case WARN: 42 | LOG.warn(unleashException.getMessage(), unleashException); 43 | break; 44 | case ERROR: 45 | LOG.error(unleashException.getMessage(), unleashException); 46 | break; 47 | case INFO: 48 | LOG.info(unleashException.getMessage(), unleashException); 49 | break; 50 | case DEBUG: 51 | LOG.debug(unleashException.getMessage(), unleashException); 52 | break; 53 | case TRACE: 54 | LOG.trace(unleashException.getMessage(), unleashException); 55 | break; 56 | } 57 | } 58 | 59 | public Log4JSubscriber setEventLevel(Level eventLevel) { 60 | this.eventLevel = eventLevel; 61 | return this; 62 | } 63 | 64 | public Log4JSubscriber setErrorLevel(Level errorLevel) { 65 | this.errorLevel = errorLevel; 66 | return this; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/NoOpSubscriber.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | public class NoOpSubscriber implements UnleashSubscriber {} 4 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/ToggleEvaluated.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | public class ToggleEvaluated implements UnleashEvent { 4 | 5 | private final String toggleName; 6 | private final boolean enabled; 7 | 8 | public ToggleEvaluated(String toggleName, boolean enabled) { 9 | this.toggleName = toggleName; 10 | this.enabled = enabled; 11 | } 12 | 13 | public String getToggleName() { 14 | return toggleName; 15 | } 16 | 17 | public boolean isEnabled() { 18 | return enabled; 19 | } 20 | 21 | @Override 22 | public void publishTo(UnleashSubscriber unleashSubscriber) { 23 | unleashSubscriber.toggleEvaluated(this); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "ToggleEvaluated: " + toggleName + "=" + enabled; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/UnleashEvent.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | public interface UnleashEvent { 4 | 5 | void publishTo(UnleashSubscriber unleashSubscriber); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/UnleashReady.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | public class UnleashReady implements UnleashEvent { 4 | 5 | @Override 6 | public void publishTo(UnleashSubscriber unleashSubscriber) { 7 | unleashSubscriber.onReady(this); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/UnleashSubscriber.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import io.getunleash.UnleashException; 4 | import io.getunleash.metric.ClientMetrics; 5 | import io.getunleash.metric.ClientRegistration; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public interface UnleashSubscriber { 9 | 10 | default void onError(UnleashException unleashException) { 11 | LoggerFactory.getLogger(UnleashSubscriber.class) 12 | .warn(unleashException.getMessage(), unleashException); 13 | } 14 | 15 | default void on(UnleashEvent unleashEvent) {} 16 | 17 | default void onReady(UnleashReady unleashReady) {} 18 | 19 | default void toggleEvaluated(ToggleEvaluated toggleEvaluated) {} 20 | 21 | default void togglesFetched(ClientFeaturesResponse toggleResponse) {} 22 | 23 | default void clientMetrics(ClientMetrics clientMetrics) {} 24 | 25 | default void clientRegistered(ClientRegistration clientRegistration) {} 26 | 27 | default void featuresBootstrapped(FeatureSet featureCollection) {} 28 | 29 | default void featuresBackedUp(FeatureSet featureCollection) {} 30 | 31 | default void featuresBackupRestored(FeatureSet featureCollection) {} 32 | 33 | default void impression(ImpressionEvent impressionEvent) {} 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/VariantImpressionEvent.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import io.getunleash.UnleashContext; 4 | 5 | public class VariantImpressionEvent extends ImpressionEvent { 6 | private String variantName; 7 | 8 | public String getVariantName() { 9 | return variantName; 10 | } 11 | 12 | public VariantImpressionEvent( 13 | String featureName, boolean enabled, UnleashContext context, String variantName) { 14 | super(featureName, enabled, context); 15 | this.variantName = variantName; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/event/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | @NonNullFields 3 | package io.getunleash.event; 4 | 5 | import io.getunleash.lang.NonNullApi; 6 | import io.getunleash.lang.NonNullFields; 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/lang/NonNullApi.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.lang; 2 | 3 | import java.lang.annotation.*; 4 | import javax.annotation.Nonnull; 5 | import javax.annotation.meta.TypeQualifierDefault; 6 | 7 | @Retention(RetentionPolicy.RUNTIME) 8 | @Documented 9 | @Nonnull 10 | @TypeQualifierDefault({ElementType.PARAMETER, ElementType.METHOD}) 11 | @Target({ElementType.PACKAGE, ElementType.TYPE}) 12 | public @interface NonNullApi {} 13 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/lang/NonNullFields.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.lang; 2 | 3 | import java.lang.annotation.*; 4 | import javax.annotation.Nonnull; 5 | import javax.annotation.meta.TypeQualifierDefault; 6 | 7 | @Retention(RetentionPolicy.RUNTIME) 8 | @Documented 9 | @Nonnull 10 | @TypeQualifierDefault(ElementType.FIELD) 11 | @Target({ElementType.PACKAGE, ElementType.TYPE}) 12 | public @interface NonNullFields {} 13 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/lang/Nullable.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.lang; 2 | 3 | import java.lang.annotation.*; 4 | import javax.annotation.Nonnull; 5 | import javax.annotation.meta.TypeQualifierNickname; 6 | import javax.annotation.meta.When; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Documented 10 | @Nonnull(when = When.MAYBE) 11 | @TypeQualifierNickname 12 | @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) 13 | public @interface Nullable {} 14 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/metric/ClientMetrics.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.metric; 2 | 3 | import io.getunleash.engine.MetricsBucket; 4 | import io.getunleash.engine.UnleashEngine; 5 | import io.getunleash.event.UnleashEvent; 6 | import io.getunleash.event.UnleashSubscriber; 7 | import io.getunleash.lang.Nullable; 8 | import io.getunleash.util.UnleashConfig; 9 | 10 | public class ClientMetrics implements UnleashEvent { 11 | 12 | private final String appName; 13 | private final String instanceId; 14 | private final String connectionId; 15 | @Nullable private final MetricsBucket bucket; 16 | private final String environment; 17 | private final String specVersion; 18 | @Nullable private final String platformName; 19 | @Nullable private final String platformVersion; 20 | @Nullable private final String yggdrasilVersion; 21 | 22 | ClientMetrics(UnleashConfig config, @Nullable MetricsBucket bucket) { 23 | this.environment = config.getEnvironment(); 24 | this.appName = config.getAppName(); 25 | this.instanceId = config.getInstanceId(); 26 | this.connectionId = config.getConnectionId(); 27 | this.bucket = bucket; 28 | this.specVersion = config.getClientSpecificationVersion(); 29 | this.platformName = System.getProperty("java.vm.name"); 30 | this.platformVersion = System.getProperty("java.version"); 31 | this.yggdrasilVersion = UnleashEngine.getCoreVersion(); 32 | } 33 | 34 | public String getAppName() { 35 | return appName; 36 | } 37 | 38 | public String getInstanceId() { 39 | return instanceId; 40 | } 41 | 42 | public String getConnectionId() { 43 | return connectionId; 44 | } 45 | 46 | @Nullable 47 | public MetricsBucket getBucket() { 48 | return bucket; 49 | } 50 | 51 | public String getEnvironment() { 52 | return environment; 53 | } 54 | 55 | public String getSpecVersion() { 56 | return specVersion; 57 | } 58 | 59 | @Nullable 60 | public String getPlatformName() { 61 | return platformName; 62 | } 63 | 64 | @Nullable 65 | public String getPlatformVersion() { 66 | return platformVersion; 67 | } 68 | 69 | @Nullable 70 | public String getYggdrasilVersion() { 71 | return yggdrasilVersion; 72 | } 73 | 74 | @Override 75 | public void publishTo(UnleashSubscriber unleashSubscriber) { 76 | unleashSubscriber.clientMetrics(this); 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return "metrics:" 82 | + " appName=" 83 | + appName 84 | + " instanceId=" 85 | + instanceId 86 | + " connectionId=" 87 | + connectionId; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/metric/ClientRegistration.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.metric; 2 | 3 | import io.getunleash.engine.UnleashEngine; 4 | import io.getunleash.event.UnleashEvent; 5 | import io.getunleash.event.UnleashSubscriber; 6 | import io.getunleash.lang.Nullable; 7 | import io.getunleash.util.UnleashConfig; 8 | import java.time.LocalDateTime; 9 | import java.util.Set; 10 | 11 | public class ClientRegistration implements UnleashEvent { 12 | private final String appName; 13 | private final String instanceId; 14 | private final String connectionId; 15 | private final String sdkVersion; 16 | private final Set strategies; 17 | private final LocalDateTime started; 18 | private final long interval; 19 | private final String environment; 20 | @Nullable private final String platformName; 21 | @Nullable private final String platformVersion; 22 | @Nullable private final String yggdrasilVersion; 23 | private final String specVersion; 24 | 25 | ClientRegistration(UnleashConfig config, LocalDateTime started, Set strategies) { 26 | this.environment = config.getEnvironment(); 27 | this.appName = config.getAppName(); 28 | this.instanceId = config.getInstanceId(); 29 | this.sdkVersion = config.getSdkVersion(); 30 | this.connectionId = config.getConnectionId(); 31 | this.started = started; 32 | this.strategies = strategies; 33 | this.interval = config.getSendMetricsInterval(); 34 | this.specVersion = config.getClientSpecificationVersion(); 35 | this.platformName = System.getProperty("java.vm.name"); 36 | this.platformVersion = System.getProperty("java.version"); 37 | this.yggdrasilVersion = UnleashEngine.getCoreVersion(); 38 | } 39 | 40 | public String getAppName() { 41 | return appName; 42 | } 43 | 44 | public String getInstanceId() { 45 | return instanceId; 46 | } 47 | 48 | public String getConnectionId() { 49 | return connectionId; 50 | } 51 | 52 | public String getSdkVersion() { 53 | return sdkVersion; 54 | } 55 | 56 | public Set getStrategies() { 57 | return strategies; 58 | } 59 | 60 | public LocalDateTime getStarted() { 61 | return started; 62 | } 63 | 64 | public long getInterval() { 65 | return interval; 66 | } 67 | 68 | public String getEnvironment() { 69 | return environment; 70 | } 71 | 72 | @Nullable 73 | public String getPlatformName() { 74 | return platformName; 75 | } 76 | 77 | @Nullable 78 | public String getPlatformVersion() { 79 | return platformVersion; 80 | } 81 | 82 | public @Nullable String getYggdrasilVersion() { 83 | return yggdrasilVersion; 84 | } 85 | 86 | public String getSpecVersion() { 87 | return specVersion; 88 | } 89 | 90 | @Override 91 | public void publishTo(UnleashSubscriber unleashSubscriber) { 92 | unleashSubscriber.clientRegistered(this); 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | return "client registration:" 98 | + " appName=" 99 | + appName 100 | + " instanceId=" 101 | + instanceId 102 | + " sdkVersion=" 103 | + sdkVersion 104 | + " started=" 105 | + sdkVersion 106 | + " interval=" 107 | + sdkVersion 108 | + " strategies=" 109 | + strategies; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.metric; 2 | 3 | import static io.getunleash.util.UnleashConfig.UNLEASH_INTERVAL; 4 | 5 | import com.google.gson.*; 6 | import io.getunleash.UnleashException; 7 | import io.getunleash.event.EventDispatcher; 8 | import io.getunleash.util.AtomicLongSerializer; 9 | import io.getunleash.util.DateTimeSerializer; 10 | import io.getunleash.util.InstantSerializer; 11 | import io.getunleash.util.UnleashConfig; 12 | import io.getunleash.util.UnleashURLs; 13 | import java.io.IOException; 14 | import java.io.OutputStreamWriter; 15 | import java.net.HttpURLConnection; 16 | import java.net.URL; 17 | import java.time.Instant; 18 | import java.time.LocalDateTime; 19 | import java.util.concurrent.atomic.AtomicLong; 20 | 21 | public class DefaultHttpMetricsSender implements MetricSender { 22 | 23 | private final Gson gson; 24 | private final EventDispatcher eventDispatcher; 25 | private UnleashConfig unleashConfig; 26 | private final URL clientRegistrationURL; 27 | private final URL clientMetricsURL; 28 | 29 | public DefaultHttpMetricsSender(UnleashConfig unleashConfig) { 30 | this.unleashConfig = unleashConfig; 31 | this.eventDispatcher = new EventDispatcher(unleashConfig); 32 | UnleashURLs urls = unleashConfig.getUnleashURLs(); 33 | this.clientMetricsURL = urls.getClientMetricsURL(); 34 | this.clientRegistrationURL = urls.getClientRegisterURL(); 35 | 36 | this.gson = 37 | new GsonBuilder() 38 | .registerTypeAdapter(LocalDateTime.class, new DateTimeSerializer()) 39 | .registerTypeAdapter(Instant.class, new InstantSerializer()) 40 | .registerTypeAdapter(AtomicLong.class, new AtomicLongSerializer()) 41 | .create(); 42 | } 43 | 44 | public int registerClient(ClientRegistration registration) { 45 | if (!unleashConfig.isDisableMetrics()) { 46 | try { 47 | int statusCode = post(clientRegistrationURL, registration); 48 | eventDispatcher.dispatch(registration); 49 | return statusCode; 50 | } catch (UnleashException ex) { 51 | eventDispatcher.dispatch(ex); 52 | return -1; 53 | } 54 | } 55 | return -1; 56 | } 57 | 58 | public int sendMetrics(ClientMetrics metrics) { 59 | if (!unleashConfig.isDisableMetrics() && metrics.getBucket() != null) { 60 | try { 61 | int statusCode = post(clientMetricsURL, metrics); 62 | eventDispatcher.dispatch(metrics); 63 | return statusCode; 64 | } catch (UnleashException ex) { 65 | eventDispatcher.dispatch(ex); 66 | return -1; 67 | } 68 | } 69 | return -1; 70 | } 71 | 72 | private int post(URL url, Object o) throws UnleashException { 73 | 74 | HttpURLConnection connection = null; 75 | try { 76 | if (this.unleashConfig.getProxy() != null) { 77 | connection = (HttpURLConnection) url.openConnection(this.unleashConfig.getProxy()); 78 | } else { 79 | connection = (HttpURLConnection) url.openConnection(); 80 | } 81 | connection.setConnectTimeout( 82 | (int) unleashConfig.getSendMetricsConnectTimeout().toMillis()); 83 | connection.setReadTimeout((int) unleashConfig.getSendMetricsReadTimeout().toMillis()); 84 | connection.setRequestMethod("POST"); 85 | connection.setRequestProperty("Accept", "application/json"); 86 | connection.setRequestProperty("Content-Type", "application/json"); 87 | connection.setRequestProperty( 88 | UNLEASH_INTERVAL, this.unleashConfig.getSendMetricsIntervalMillis()); 89 | UnleashConfig.setRequestProperties(connection, this.unleashConfig); 90 | connection.setUseCaches(false); 91 | connection.setDoInput(true); 92 | connection.setDoOutput(true); 93 | 94 | OutputStreamWriter wr = new OutputStreamWriter(connection.getOutputStream()); 95 | gson.toJson(o, wr); 96 | wr.flush(); 97 | wr.close(); 98 | 99 | connection.connect(); 100 | 101 | // TODO should probably check response code to detect errors? 102 | return connection.getResponseCode(); 103 | } catch (IOException e) { 104 | throw new UnleashException("Could not post to Unleash API", e); 105 | } catch (IllegalStateException e) { 106 | throw new UnleashException(e.getMessage(), e); 107 | } finally { 108 | if (connection != null) { 109 | connection.disconnect(); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/metric/MetricSender.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.metric; 2 | 3 | public interface MetricSender { 4 | int registerClient(ClientRegistration registration); 5 | 6 | int sendMetrics(ClientMetrics metrics); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/metric/OkHttpMetricsSender.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.metric; 2 | 3 | import static io.getunleash.util.UnleashConfig.UNLEASH_INTERVAL; 4 | 5 | import com.google.gson.Gson; 6 | import com.google.gson.GsonBuilder; 7 | import io.getunleash.UnleashException; 8 | import io.getunleash.event.EventDispatcher; 9 | import io.getunleash.util.AtomicLongSerializer; 10 | import io.getunleash.util.DateTimeSerializer; 11 | import io.getunleash.util.OkHttpClientConfigurer; 12 | import io.getunleash.util.UnleashConfig; 13 | import java.io.IOException; 14 | import java.time.LocalDateTime; 15 | import java.util.Objects; 16 | import java.util.concurrent.atomic.AtomicLong; 17 | import okhttp3.HttpUrl; 18 | import okhttp3.MediaType; 19 | import okhttp3.OkHttpClient; 20 | import okhttp3.Request; 21 | import okhttp3.RequestBody; 22 | import okhttp3.Response; 23 | 24 | public class OkHttpMetricsSender implements MetricSender { 25 | private final UnleashConfig config; 26 | private final MediaType JSON = 27 | Objects.requireNonNull(MediaType.parse("application/json; charset=utf-8")); 28 | 29 | private final EventDispatcher eventDispatcher; 30 | private final OkHttpClient client; 31 | 32 | private final Gson gson; 33 | 34 | private final HttpUrl clientRegistrationUrl; 35 | 36 | private final HttpUrl clientMetricsUrl; 37 | 38 | public OkHttpMetricsSender(UnleashConfig config) { 39 | this.config = config; 40 | this.clientMetricsUrl = 41 | Objects.requireNonNull(HttpUrl.get(config.getUnleashURLs().getClientMetricsURL())); 42 | this.clientRegistrationUrl = 43 | Objects.requireNonNull(HttpUrl.get(config.getUnleashURLs().getClientRegisterURL())); 44 | this.eventDispatcher = new EventDispatcher(config); 45 | 46 | OkHttpClient.Builder builder; 47 | if (config.getProxy() != null) { 48 | builder = new OkHttpClient.Builder().proxy(config.getProxy()); 49 | } else { 50 | builder = new OkHttpClient.Builder(); 51 | } 52 | builder = 53 | builder.callTimeout(config.getSendMetricsConnectTimeout()) 54 | .readTimeout(config.getSendMetricsReadTimeout()); 55 | this.client = OkHttpClientConfigurer.configureInterceptor(config, builder.build()); 56 | 57 | this.gson = 58 | new GsonBuilder() 59 | .registerTypeAdapter(LocalDateTime.class, new DateTimeSerializer()) 60 | .registerTypeAdapter(AtomicLong.class, new AtomicLongSerializer()) 61 | .create(); 62 | } 63 | 64 | @Override 65 | public int registerClient(ClientRegistration registration) { 66 | if (!config.isDisableMetrics()) { 67 | try { 68 | int statusCode = post(clientRegistrationUrl, registration); 69 | eventDispatcher.dispatch(registration); 70 | return statusCode; 71 | } catch (UnleashException ex) { 72 | eventDispatcher.dispatch(ex); 73 | } 74 | } 75 | return -1; 76 | } 77 | 78 | @Override 79 | public int sendMetrics(ClientMetrics metrics) { 80 | if (!config.isDisableMetrics()) { 81 | try { 82 | post(clientMetricsUrl, metrics); 83 | eventDispatcher.dispatch(metrics); 84 | } catch (UnleashException ex) { 85 | eventDispatcher.dispatch(ex); 86 | } 87 | } 88 | return -1; 89 | } 90 | 91 | private int post(HttpUrl url, Object o) { 92 | RequestBody body = RequestBody.create(gson.toJson(o), JSON); 93 | Request request = 94 | new Request.Builder() 95 | .url(url) 96 | .post(body) 97 | .addHeader(UNLEASH_INTERVAL, config.getSendMetricsIntervalMillis()) 98 | .build(); 99 | try (Response response = this.client.newCall(request).execute()) { 100 | return response.code(); 101 | } catch (IOException ioEx) { 102 | throw new UnleashException("Could not post to Unleash API", ioEx); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/metric/UnleashMetricService.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.metric; 2 | 3 | import java.util.Set; 4 | 5 | public interface UnleashMetricService { 6 | void register(Set strategies); 7 | 8 | void countToggle(String name, boolean enabled); 9 | 10 | void countVariant(String name, String variantName); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.metric; 2 | 3 | import io.getunleash.engine.MetricsBucket; 4 | import io.getunleash.engine.UnleashEngine; 5 | import io.getunleash.engine.YggdrasilError; 6 | import io.getunleash.util.Throttler; 7 | import io.getunleash.util.UnleashConfig; 8 | import io.getunleash.util.UnleashScheduledExecutor; 9 | import java.time.LocalDateTime; 10 | import java.time.ZoneId; 11 | import java.util.Set; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | public class UnleashMetricServiceImpl implements UnleashMetricService { 16 | private static final Logger LOGGER = LoggerFactory.getLogger(UnleashMetricServiceImpl.class); 17 | private final LocalDateTime started; 18 | private final UnleashConfig unleashConfig; 19 | private final MetricSender metricSender; 20 | 21 | // synchronization is handled in the engine itself 22 | private final UnleashEngine engine; 23 | 24 | private final Throttler throttler; 25 | 26 | public UnleashMetricServiceImpl( 27 | UnleashConfig unleashConfig, UnleashScheduledExecutor executor, UnleashEngine engine) { 28 | this( 29 | unleashConfig, 30 | unleashConfig.getMetricSenderFactory().apply(unleashConfig), 31 | executor, 32 | engine); 33 | } 34 | 35 | public UnleashMetricServiceImpl( 36 | UnleashConfig unleashConfig, 37 | MetricSender metricSender, 38 | UnleashScheduledExecutor executor, 39 | UnleashEngine engine) { 40 | this.started = LocalDateTime.now(ZoneId.of("UTC")); 41 | this.unleashConfig = unleashConfig; 42 | this.metricSender = metricSender; 43 | this.throttler = 44 | new Throttler( 45 | (int) unleashConfig.getSendMetricsInterval(), 46 | 300, 47 | unleashConfig.getUnleashURLs().getClientMetricsURL()); 48 | this.engine = engine; 49 | long metricsInterval = unleashConfig.getSendMetricsInterval(); 50 | 51 | executor.setInterval(sendMetrics(), metricsInterval, metricsInterval); 52 | } 53 | 54 | @Override 55 | public void register(Set strategies) { 56 | ClientRegistration registration = 57 | new ClientRegistration(unleashConfig, started, strategies); 58 | metricSender.registerClient(registration); 59 | } 60 | 61 | private Runnable sendMetrics() { 62 | return () -> { 63 | if (throttler.performAction()) { 64 | try { 65 | MetricsBucket bucket = this.engine.getMetrics(); 66 | 67 | ClientMetrics metrics = new ClientMetrics(unleashConfig, bucket); 68 | int statusCode = metricSender.sendMetrics(metrics); 69 | if (statusCode >= 200 && statusCode < 400) { 70 | throttler.decrementFailureCountAndResetSkips(); 71 | } 72 | if (statusCode >= 400) { 73 | throttler.handleHttpErrorCodes(statusCode); 74 | } 75 | } catch (YggdrasilError e) { 76 | LOGGER.error( 77 | "Failed to retrieve metrics from the engine, this is a serious error, please report it", 78 | e); 79 | } 80 | } else { 81 | throttler.skipped(); 82 | } 83 | }; 84 | } 85 | 86 | protected int getSkips() { 87 | return this.throttler.getSkips(); 88 | } 89 | 90 | protected int getFailures() { 91 | return this.throttler.getFailures(); 92 | } 93 | 94 | @Override 95 | public void countToggle(String name, boolean enabled) { 96 | try { 97 | this.engine.countToggle(name, enabled); 98 | } catch (YggdrasilError e) { 99 | LOGGER.error("Failed to count toggle", e); 100 | } 101 | } 102 | 103 | @Override 104 | public void countVariant(String name, String variantName) { 105 | try { 106 | this.engine.countVariant(name, variantName); 107 | } catch (YggdrasilError e) { 108 | LOGGER.error("Failed to count variant", e); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/metric/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | @NonNullFields 3 | package io.getunleash.metric; 4 | 5 | import io.getunleash.lang.NonNullApi; 6 | import io.getunleash.lang.NonNullFields; 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | @NonNullFields 3 | package io.getunleash; 4 | 5 | import io.getunleash.lang.NonNullApi; 6 | import io.getunleash.lang.NonNullFields; 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/BackupHandler.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import java.util.Optional; 4 | 5 | public interface BackupHandler { 6 | Optional read(); 7 | 8 | void write(String collection); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/FeatureBackupHandlerFile.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import com.google.gson.JsonParseException; 4 | import io.getunleash.UnleashException; 5 | import io.getunleash.event.EventDispatcher; 6 | import io.getunleash.event.FeatureSet; 7 | import io.getunleash.event.UnleashEvent; 8 | import io.getunleash.event.UnleashSubscriber; 9 | import io.getunleash.util.UnleashConfig; 10 | import java.io.*; 11 | import java.util.Optional; 12 | import java.util.stream.Collectors; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | public class FeatureBackupHandlerFile implements BackupHandler { 17 | private static final Logger LOG = LoggerFactory.getLogger(FeatureBackupHandlerFile.class); 18 | 19 | private final String backupFile; 20 | private final EventDispatcher eventDispatcher; 21 | 22 | public FeatureBackupHandlerFile(UnleashConfig config) { 23 | this.backupFile = config.getBackupFile(); 24 | this.eventDispatcher = new EventDispatcher(config); 25 | } 26 | 27 | @Override 28 | public Optional read() { 29 | LOG.info("Unleash will try to load feature toggle states from temporary backup"); 30 | try (BufferedReader reader = new BufferedReader(new FileReader(backupFile))) { 31 | String clientFeatures = reader.lines().collect(Collectors.joining("\n")); 32 | 33 | eventDispatcher.dispatch(new FeatureBackupRead(clientFeatures)); 34 | return Optional.of(clientFeatures); 35 | } catch (FileNotFoundException e) { 36 | LOG.info( 37 | " Unleash could not find the backup-file '" 38 | + backupFile 39 | + "'. \n" 40 | + "This is expected behavior the first time unleash runs in a new environment."); 41 | return Optional.empty(); 42 | } catch (IOException | IllegalStateException | JsonParseException e) { 43 | eventDispatcher.dispatch( 44 | new UnleashException("Failed to read backup file: " + backupFile, e)); 45 | return Optional.empty(); 46 | } 47 | } 48 | 49 | @Override 50 | public void write(String features) { 51 | try (FileWriter writer = new FileWriter(backupFile)) { 52 | writer.write(features); 53 | eventDispatcher.dispatch(new FeatureBackupWritten(features)); 54 | } catch (IOException e) { 55 | eventDispatcher.dispatch( 56 | new UnleashException( 57 | "Unleash was unable to backup feature toggles to file: " + backupFile, 58 | e)); 59 | } 60 | } 61 | 62 | private static class FeatureBackupRead implements UnleashEvent { 63 | 64 | private final String featureCollection; 65 | 66 | private FeatureBackupRead(String featureCollection) { 67 | this.featureCollection = featureCollection; 68 | } 69 | 70 | @Override 71 | public void publishTo(UnleashSubscriber unleashSubscriber) { 72 | unleashSubscriber.featuresBackupRestored(new FeatureSet(featureCollection)); 73 | } 74 | } 75 | 76 | private static class FeatureBackupWritten implements UnleashEvent { 77 | 78 | private final String featureCollection; 79 | 80 | private FeatureBackupWritten(String featureCollection) { 81 | this.featureCollection = featureCollection; 82 | } 83 | 84 | @Override 85 | public void publishTo(UnleashSubscriber unleashSubscriber) { 86 | unleashSubscriber.featuresBackedUp((new FeatureSet(featureCollection))); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/FeatureFetcher.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import io.getunleash.UnleashException; 4 | import io.getunleash.event.ClientFeaturesResponse; 5 | 6 | public interface FeatureFetcher { 7 | ClientFeaturesResponse fetchFeatures() throws UnleashException; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/FeatureRepository.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import io.getunleash.FeatureDefinition; 4 | import io.getunleash.UnleashContext; 5 | import io.getunleash.engine.VariantDef; 6 | import java.util.Optional; 7 | import java.util.stream.Stream; 8 | 9 | public interface FeatureRepository { 10 | 11 | Boolean isEnabled(String toggleName, UnleashContext context); 12 | 13 | Optional getVariant(String toggleName, UnleashContext context); 14 | 15 | Stream listKnownToggles(); 16 | 17 | boolean shouldEmitImpressionEvent(String toggleName); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/HttpFeatureFetcher.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import static io.getunleash.util.UnleashConfig.UNLEASH_INTERVAL; 4 | 5 | import io.getunleash.UnleashException; 6 | import io.getunleash.event.ClientFeaturesResponse; 7 | import io.getunleash.util.UnleashConfig; 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.net.HttpURLConnection; 13 | import java.net.URL; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.Optional; 16 | import java.util.stream.Collectors; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | public class HttpFeatureFetcher implements FeatureFetcher { 21 | private static final Logger LOG = LoggerFactory.getLogger(HttpFeatureFetcher.class); 22 | private Optional etag = Optional.empty(); 23 | 24 | private final UnleashConfig config; 25 | 26 | private final URL toggleUrl; 27 | 28 | public HttpFeatureFetcher(UnleashConfig config) { 29 | this.config = config; 30 | this.toggleUrl = 31 | config.getUnleashURLs() 32 | .getFetchTogglesURL(config.getProjectName(), config.getNamePrefix()); 33 | } 34 | 35 | @Override 36 | public ClientFeaturesResponse fetchFeatures() throws UnleashException { 37 | HttpURLConnection connection = null; 38 | try { 39 | connection = openConnection(this.toggleUrl); 40 | connection.setRequestProperty( 41 | UNLEASH_INTERVAL, this.config.getFetchTogglesIntervalMillis()); 42 | connection.connect(); 43 | 44 | return getFeatureResponse(connection, true); 45 | } catch (IOException e) { 46 | throw new UnleashException("Could not fetch toggles", e); 47 | } catch (IllegalStateException e) { 48 | throw new UnleashException(e.getMessage(), e); 49 | } finally { 50 | if (connection != null) { 51 | connection.disconnect(); 52 | } 53 | } 54 | } 55 | 56 | private ClientFeaturesResponse getFeatureResponse( 57 | HttpURLConnection request, boolean followRedirect) throws IOException { 58 | int responseCode = request.getResponseCode(); 59 | 60 | if (responseCode < 300) { 61 | etag = Optional.ofNullable(request.getHeaderField("ETag")); 62 | 63 | try (BufferedReader reader = 64 | new BufferedReader( 65 | new InputStreamReader( 66 | (InputStream) request.getContent(), StandardCharsets.UTF_8))) { 67 | 68 | String clientFeatures = reader.lines().collect(Collectors.joining("\n")); 69 | 70 | return ClientFeaturesResponse.updated(clientFeatures); 71 | } 72 | } else if (followRedirect 73 | && (responseCode == HttpURLConnection.HTTP_MOVED_TEMP 74 | || responseCode == HttpURLConnection.HTTP_MOVED_PERM 75 | || responseCode == HttpURLConnection.HTTP_SEE_OTHER)) { 76 | return followRedirect(request); 77 | } else if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { 78 | return ClientFeaturesResponse.notChanged(); 79 | } else { 80 | return ClientFeaturesResponse.unavailable(responseCode, getLocationHeader(request)); 81 | } 82 | } 83 | 84 | private ClientFeaturesResponse followRedirect(HttpURLConnection request) throws IOException { 85 | String newUrl = 86 | getLocationHeader(request) 87 | .orElseThrow( 88 | () -> 89 | new IllegalStateException( 90 | "No Location header found in redirect response.")); 91 | 92 | request = openConnection(new URL(newUrl)); 93 | request.connect(); 94 | LOG.info( 95 | "Redirecting from {} to {}. Please consider updating your config.", 96 | this.toggleUrl, 97 | newUrl); 98 | 99 | return getFeatureResponse(request, false); 100 | } 101 | 102 | private Optional getLocationHeader(HttpURLConnection connection) { 103 | return Optional.ofNullable(connection.getHeaderField("Location")); 104 | } 105 | 106 | private HttpURLConnection openConnection(URL url) throws IOException { 107 | HttpURLConnection connection; 108 | if (this.config.getProxy() != null) { 109 | connection = (HttpURLConnection) url.openConnection(this.config.getProxy()); 110 | } else { 111 | connection = (HttpURLConnection) url.openConnection(); 112 | } 113 | connection.setConnectTimeout((int) this.config.getFetchTogglesConnectTimeout().toMillis()); 114 | connection.setReadTimeout((int) this.config.getFetchTogglesReadTimeout().toMillis()); 115 | connection.setRequestProperty("Accept", "application/json"); 116 | connection.setRequestProperty("Content-Type", "application/json"); 117 | UnleashConfig.setRequestProperties(connection, this.config); 118 | 119 | etag.ifPresent(val -> connection.setRequestProperty("If-None-Match", val)); 120 | 121 | connection.setUseCaches(true); 122 | 123 | return connection; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/OkHttpFeatureFetcher.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import static io.getunleash.util.UnleashConfig.UNLEASH_INTERVAL; 4 | 5 | import com.google.gson.JsonSyntaxException; 6 | import io.getunleash.UnleashException; 7 | import io.getunleash.event.ClientFeaturesResponse; 8 | import io.getunleash.util.OkHttpClientConfigurer; 9 | import io.getunleash.util.UnleashConfig; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.util.Objects; 14 | import java.util.Optional; 15 | import okhttp3.Cache; 16 | import okhttp3.HttpUrl; 17 | import okhttp3.OkHttpClient; 18 | import okhttp3.Request; 19 | import okhttp3.Response; 20 | 21 | public class OkHttpFeatureFetcher implements FeatureFetcher { 22 | private final HttpUrl toggleUrl; 23 | private final OkHttpClient client; 24 | private final String interval; 25 | 26 | public OkHttpFeatureFetcher(UnleashConfig unleashConfig) { 27 | this.interval = unleashConfig.getFetchTogglesIntervalMillis(); 28 | File tempDir = null; 29 | try { 30 | tempDir = Files.createTempDirectory("http_cache").toFile(); 31 | } catch (IOException ignored) { 32 | } 33 | OkHttpClient.Builder builder = 34 | new OkHttpClient.Builder() 35 | .connectTimeout(unleashConfig.getFetchTogglesConnectTimeout()) 36 | .callTimeout(unleashConfig.getFetchTogglesReadTimeout()) 37 | .followRedirects(true); 38 | if (tempDir != null) { 39 | builder = builder.cache(new Cache(tempDir, 1024 * 1024 * 50)); 40 | } 41 | if (unleashConfig.getProxy() != null) { 42 | builder = builder.proxy(unleashConfig.getProxy()); 43 | } 44 | 45 | this.toggleUrl = 46 | Objects.requireNonNull( 47 | HttpUrl.get( 48 | unleashConfig 49 | .getUnleashURLs() 50 | .getFetchTogglesURL( 51 | unleashConfig.getProjectName(), 52 | unleashConfig.getNamePrefix()))); 53 | this.client = OkHttpClientConfigurer.configureInterceptor(unleashConfig, builder.build()); 54 | } 55 | 56 | public OkHttpFeatureFetcher(UnleashConfig unleashConfig, OkHttpClient client) { 57 | this.interval = unleashConfig.getFetchTogglesIntervalMillis(); 58 | this.client = OkHttpClientConfigurer.configureInterceptor(unleashConfig, client); 59 | this.toggleUrl = 60 | Objects.requireNonNull( 61 | HttpUrl.get( 62 | unleashConfig 63 | .getUnleashURLs() 64 | .getFetchTogglesURL( 65 | unleashConfig.getProjectName(), 66 | unleashConfig.getNamePrefix()))); 67 | } 68 | 69 | @Override 70 | public ClientFeaturesResponse fetchFeatures() throws UnleashException { 71 | Request request = 72 | new Request.Builder() 73 | .url(toggleUrl) 74 | .get() 75 | .addHeader(UNLEASH_INTERVAL, interval) 76 | .build(); 77 | int code = 200; 78 | try (Response response = client.newCall(request).execute()) { 79 | if (response.isSuccessful()) { 80 | if (response.networkResponse() != null 81 | && response.networkResponse().code() == 304) { 82 | return ClientFeaturesResponse.notChanged(); 83 | } 84 | String features = response.body().string(); 85 | 86 | return ClientFeaturesResponse.updated(features); 87 | } else if (response.code() == 304) { 88 | return ClientFeaturesResponse.notChanged(); 89 | } else { 90 | return ClientFeaturesResponse.unavailable( 91 | response.code(), Optional.of(toggleUrl.toString())); 92 | } 93 | } catch (IOException | NullPointerException ioEx) { 94 | throw new UnleashException("Could not fetch toggles", ioEx); 95 | } catch (IllegalStateException | JsonSyntaxException ex) { 96 | return ClientFeaturesResponse.unavailable(code, Optional.of(toggleUrl.toString())); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/ToggleBootstrapFileProvider.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import io.getunleash.lang.Nullable; 4 | import java.io.File; 5 | import java.io.FileNotFoundException; 6 | import java.io.IOException; 7 | import java.net.URISyntaxException; 8 | import java.net.URL; 9 | import java.nio.charset.StandardCharsets; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.Optional; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | public class ToggleBootstrapFileProvider implements ToggleBootstrapProvider { 17 | private static final Logger LOG = LoggerFactory.getLogger(ToggleBootstrapFileProvider.class); 18 | final String path; 19 | 20 | public ToggleBootstrapFileProvider() { 21 | this.path = getBootstrapFile(); 22 | } 23 | 24 | /** 25 | * Accepts path to file to read either as constructor parameter or as environment variable in 26 | * "UNLEASH_BOOTSTRAP_FILE" 27 | * 28 | * @param path - path to toggles file 29 | */ 30 | public ToggleBootstrapFileProvider(String path) { 31 | this.path = path; 32 | } 33 | 34 | @Override 35 | public Optional read() { 36 | LOG.info("Trying to read feature toggles from bootstrap file found at {}", path); 37 | try { 38 | File file = getFile(path); 39 | if (file != null) { 40 | return Optional.of(fileAsString(file)); 41 | } 42 | } catch (FileNotFoundException ioEx) { 43 | LOG.warn("Could not find file {}", path, ioEx); 44 | } catch (IOException ioEx) { 45 | LOG.warn("Generic IOException when trying to read file at {}", path, ioEx); 46 | } 47 | return Optional.empty(); 48 | } 49 | 50 | @Nullable 51 | private String getBootstrapFile() { 52 | String path = System.getenv("UNLEASH_BOOTSTRAP_FILE"); 53 | if (path == null) { 54 | path = System.getProperty("UNLEASH_BOOTSTRAP_FILE"); 55 | } 56 | return path; 57 | } 58 | 59 | private String fileAsString(File file) throws IOException { 60 | return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); 61 | } 62 | 63 | @Nullable 64 | private File getFile(@Nullable String path) { 65 | if (path != null) { 66 | if (path.startsWith("classpath:")) { 67 | try { 68 | URL resource = 69 | getClass() 70 | .getClassLoader() 71 | .getResource(path.substring("classpath:".length())); 72 | if (resource != null) { 73 | return Paths.get(resource.toURI()).toFile(); 74 | } 75 | return null; 76 | } catch (URISyntaxException e) { 77 | return null; 78 | } 79 | } else { 80 | return Paths.get(path).toFile(); 81 | } 82 | } else { 83 | return null; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/ToggleBootstrapProvider.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import java.util.Optional; 4 | 5 | public interface ToggleBootstrapProvider { 6 | /** 7 | * Should return JSON string parsable to /api/client/features format Look in 8 | * src/test/resources/features-v1.json or src/test/resources/unleash-repo-v1.json for example 9 | * Example in {@link ToggleBootstrapFileProvider} 10 | * 11 | * @return JSON string representing a response from /api/client/features 12 | */ 13 | Optional read(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/YggdrasilAdapters.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import io.getunleash.DefaultUnleash; 4 | import io.getunleash.UnleashContext; 5 | import io.getunleash.engine.Context; 6 | import io.getunleash.engine.IStrategy; 7 | import io.getunleash.engine.Payload; 8 | import io.getunleash.engine.VariantDef; 9 | import io.getunleash.lang.Nullable; 10 | import io.getunleash.strategy.Strategy; 11 | import io.getunleash.variant.Variant; 12 | import java.time.ZonedDateTime; 13 | import java.time.format.DateTimeFormatter; 14 | import java.time.format.DateTimeParseException; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import org.jetbrains.annotations.NotNull; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | public final class YggdrasilAdapters { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUnleash.class); 24 | 25 | @NotNull 26 | public static IStrategy adapt(Strategy s) { 27 | return new IStrategy() { 28 | @Override 29 | public String getName() { 30 | return s.getName(); 31 | } 32 | 33 | @Override 34 | public boolean isEnabled(Map map, Context context) { 35 | return s.isEnabled(map, adapt(context)); 36 | } 37 | }; 38 | } 39 | 40 | public static UnleashContext adapt(Context context) { 41 | ZonedDateTime currentTime = ZonedDateTime.now(); 42 | if (context.getCurrentTime() != null) { 43 | try { 44 | currentTime = ZonedDateTime.parse(context.getCurrentTime()); 45 | } catch (DateTimeParseException e) { 46 | LOGGER.warn( 47 | "Could not parse current time from context, falling back to system time: ", 48 | context.getCurrentTime()); 49 | currentTime = ZonedDateTime.now(); 50 | } 51 | } 52 | 53 | return new UnleashContext( 54 | context.getAppName(), 55 | context.getEnvironment(), 56 | context.getUserId(), 57 | context.getSessionId(), 58 | context.getRemoteAddress(), 59 | currentTime, 60 | context.getProperties()); 61 | } 62 | 63 | public static Context adapt(UnleashContext context) { 64 | Context mapped = new Context(); 65 | mapped.setAppName(context.getAppName().orElse(null)); 66 | mapped.setEnvironment(context.getEnvironment().orElse(null)); 67 | mapped.setUserId(context.getUserId().orElse(null)); 68 | mapped.setSessionId(context.getSessionId().orElse(null)); 69 | mapped.setRemoteAddress(context.getRemoteAddress().orElse(null)); 70 | mapped.setProperties(context.getProperties()); 71 | mapped.setCurrentTime( 72 | DateTimeFormatter.ISO_INSTANT.format( 73 | context.getCurrentTime().orElse(ZonedDateTime.now()).toInstant())); 74 | return mapped; 75 | } 76 | 77 | public static Variant adapt(Optional variant, Variant defaultValue) { 78 | if (!variant.isPresent()) { 79 | return defaultValue; 80 | } 81 | VariantDef unwrapped = variant.get(); 82 | return new Variant( 83 | unwrapped.getName(), 84 | adapt(unwrapped.getPayload()), 85 | unwrapped.isEnabled(), 86 | unwrapped.isFeatureEnabled()); 87 | } 88 | 89 | public static @Nullable io.getunleash.variant.Payload adapt(@Nullable Payload payload) { 90 | return Optional.ofNullable(payload) 91 | .map(p -> new io.getunleash.variant.Payload(p.getType(), p.getValue())) 92 | .orElse(new io.getunleash.variant.Payload("string", null)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/repository/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | @NonNullFields 3 | package io.getunleash.repository; 4 | 5 | import io.getunleash.lang.NonNullApi; 6 | import io.getunleash.lang.NonNullFields; 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/strategy/Strategy.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.strategy; 2 | 3 | import io.getunleash.UnleashContext; 4 | import java.util.Map; 5 | 6 | public interface Strategy { 7 | 8 | String getName(); 9 | 10 | boolean isEnabled(Map parameters, UnleashContext context); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/strategy/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullFields 2 | @NonNullApi 3 | package io.getunleash.strategy; 4 | 5 | import io.getunleash.lang.NonNullApi; 6 | import io.getunleash.lang.NonNullFields; 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/AtomicLongSerializer.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonPrimitive; 5 | import com.google.gson.JsonSerializationContext; 6 | import com.google.gson.JsonSerializer; 7 | import java.lang.reflect.Type; 8 | import java.util.concurrent.atomic.AtomicLong; 9 | 10 | public class AtomicLongSerializer implements JsonSerializer { 11 | 12 | @Override 13 | public JsonElement serialize(AtomicLong src, Type typeOfSrc, JsonSerializationContext context) { 14 | return new JsonPrimitive(src.get()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/ClientFeaturesParser.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonArray; 6 | import com.google.gson.JsonObject; 7 | import com.google.gson.reflect.TypeToken; 8 | import io.getunleash.FeatureDefinition; 9 | import java.lang.reflect.Type; 10 | import java.util.List; 11 | 12 | public class ClientFeaturesParser { 13 | 14 | private static Gson gson = 15 | new GsonBuilder() 16 | .registerTypeAdapter(FeatureDefinition.class, new FeatureDefinitionAdapter()) 17 | .create(); 18 | 19 | public static List parse(String clientFeatures) { 20 | JsonObject jsonObject = gson.fromJson(clientFeatures, JsonObject.class); 21 | 22 | JsonArray featuresArray = jsonObject.getAsJsonArray("features"); 23 | 24 | Type listType = new TypeToken>() {}.getType(); 25 | return gson.fromJson(featuresArray, listType); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/DateTimeSerializer.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import static java.time.format.DateTimeFormatter.ISO_INSTANT; 4 | 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonPrimitive; 7 | import com.google.gson.JsonSerializationContext; 8 | import com.google.gson.JsonSerializer; 9 | import java.lang.reflect.Type; 10 | import java.time.LocalDateTime; 11 | import java.time.ZoneOffset; 12 | 13 | public class DateTimeSerializer implements JsonSerializer { 14 | @Override 15 | public JsonElement serialize( 16 | LocalDateTime localDateTime, 17 | Type type, 18 | JsonSerializationContext jsonSerializationContext) { 19 | return new JsonPrimitive(ISO_INSTANT.format(localDateTime.toInstant(ZoneOffset.UTC))); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/FeatureDefinitionAdapter.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import com.google.gson.TypeAdapter; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonWriter; 6 | import io.getunleash.FeatureDefinition; 7 | import java.io.IOException; 8 | import java.util.Optional; 9 | 10 | public class FeatureDefinitionAdapter extends TypeAdapter { 11 | 12 | @Override 13 | public void write(JsonWriter out, FeatureDefinition value) throws IOException { 14 | out.beginObject(); 15 | out.name("name").value(value.getName()); 16 | out.name("project").value(value.getProject()); 17 | out.name("type").value(value.getType().orElse(null)); 18 | out.name("enabled").value(value.environmentEnabled()); 19 | out.endObject(); 20 | } 21 | 22 | @Override 23 | public FeatureDefinition read(JsonReader in) throws IOException { 24 | String name = null; 25 | String project = null; 26 | Optional type = Optional.empty(); 27 | boolean enabled = false; 28 | 29 | in.beginObject(); 30 | while (in.hasNext()) { 31 | switch (in.nextName()) { 32 | case "name": 33 | name = in.nextString(); 34 | break; 35 | case "project": 36 | project = in.nextString(); 37 | break; 38 | case "type": 39 | type = Optional.of(in.nextString()); 40 | break; 41 | case "enabled": 42 | enabled = in.nextBoolean(); 43 | break; 44 | default: 45 | in.skipValue(); 46 | } 47 | } 48 | in.endObject(); 49 | 50 | if (name == null) { 51 | throw new IOException("Missing required field 'name'"); 52 | } 53 | 54 | return new FeatureDefinition(name, type, project, enabled); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/InstantSerializer.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import com.google.gson.*; 4 | import java.lang.reflect.Type; 5 | import java.time.Instant; 6 | 7 | public class InstantSerializer implements JsonSerializer, JsonDeserializer { 8 | 9 | @Override 10 | public JsonElement serialize( 11 | Instant instant, Type typeOfSrc, JsonSerializationContext context) { 12 | return new JsonPrimitive(instant.toString()); 13 | } 14 | 15 | @Override 16 | public Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) 17 | throws JsonParseException { 18 | return Instant.parse(json.getAsString()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/MetricSenderFactory.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import io.getunleash.metric.MetricSender; 4 | import java.util.function.Function; 5 | 6 | public interface MetricSenderFactory extends Function {} 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/OkHttpClientConfigurer.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import static io.getunleash.util.UnleashConfig.*; 4 | 5 | import java.util.Map; 6 | import okhttp3.OkHttpClient; 7 | import okhttp3.Request; 8 | 9 | public class OkHttpClientConfigurer { 10 | public static OkHttpClient configureInterceptor(UnleashConfig config, OkHttpClient client) { 11 | return client.newBuilder() 12 | .addInterceptor( 13 | (c) -> { 14 | Request.Builder headers = 15 | c.request() 16 | .newBuilder() 17 | .addHeader("Content-Type", "application/json") 18 | .addHeader("Accept", "application/json") 19 | .addHeader(UNLEASH_APP_NAME_HEADER, config.getAppName()) 20 | .addHeader( 21 | UNLEASH_INSTANCE_ID_HEADER, 22 | config.getInstanceId()) 23 | .addHeader( 24 | UNLEASH_CONNECTION_ID_HEADER, 25 | config.getConnectionId()) 26 | .addHeader(UNLEASH_SDK_HEADER, config.getSdkVersion()) 27 | .addHeader("User-Agent", config.getAppName()) 28 | .addHeader( 29 | "Unleash-Client-Spec", 30 | config.getClientSpecificationVersion()); 31 | for (Map.Entry headerEntry : 32 | config.getCustomHttpHeaders().entrySet()) { 33 | headers = 34 | headers.addHeader( 35 | headerEntry.getKey(), headerEntry.getValue()); 36 | } 37 | for (Map.Entry headerEntry : 38 | config.getCustomHttpHeadersProvider() 39 | .getCustomHeaders() 40 | .entrySet()) { 41 | headers = 42 | headers.addHeader( 43 | headerEntry.getKey(), headerEntry.getValue()); 44 | } 45 | return c.proceed(headers.build()); 46 | }) 47 | .build(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/Throttler.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import static java.lang.Integer.max; 4 | 5 | import java.net.HttpURLConnection; 6 | import java.net.URL; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class Throttler { 12 | private static final Logger LOGGER = LoggerFactory.getLogger(Throttler.class); 13 | private final int maxSkips; 14 | 15 | private final int intervalLength; 16 | private final AtomicInteger skips = new AtomicInteger(0); 17 | private final AtomicInteger failures = new AtomicInteger(0); 18 | 19 | private final URL target; 20 | 21 | public Throttler(int intervalLengthSeconds, int longestAcceptableIntervalSeconds, URL target) { 22 | this.maxSkips = max(longestAcceptableIntervalSeconds / max(intervalLengthSeconds, 1), 1); 23 | this.target = target; 24 | this.intervalLength = intervalLengthSeconds; 25 | } 26 | 27 | /** 28 | * We've had one successful call, so if we had 10 failures in a row, this will reduce the skips 29 | * down to 9, so that we gradually start polling more often, instead of doing max load 30 | * immediately after a sequence of errors. 31 | */ 32 | public void decrementFailureCountAndResetSkips() { 33 | if (failures.get() > 0) { 34 | skips.set(Math.max(failures.decrementAndGet(), 0)); 35 | } 36 | } 37 | 38 | /** 39 | * We've gotten the message to back off (usually a 429 or a 50x). If we have successive 40 | * failures, failure count here will be incremented higher and higher which will handle 41 | * increasing our backoff, since we set the skip count to the failure count after every reset 42 | */ 43 | public void increaseSkipCount() { 44 | skips.set(Math.min(failures.incrementAndGet(), maxSkips)); 45 | } 46 | 47 | /** 48 | * We've received an error code that we don't expect to change, which means we've already logged 49 | * an ERROR. To avoid hammering the server that just told us we did something wrong and to avoid 50 | * flooding the logs, we'll increase our skip count to maximum 51 | */ 52 | public void maximizeSkips() { 53 | skips.set(maxSkips); 54 | failures.incrementAndGet(); 55 | } 56 | 57 | public boolean performAction() { 58 | return skips.get() <= 0; 59 | } 60 | 61 | public void skipped() { 62 | skips.decrementAndGet(); 63 | } 64 | 65 | public void handleHttpErrorCodes(int responseCode) { 66 | if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED 67 | || responseCode == HttpURLConnection.HTTP_FORBIDDEN) { 68 | maximizeSkips(); 69 | LOGGER.error( 70 | "Client was not authorized to talk to the Unleash API at {}. Backing off to {} times our poll interval (of {} seconds) to avoid overloading server", 71 | this.target, 72 | maxSkips, 73 | this.intervalLength); 74 | } 75 | if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { 76 | maximizeSkips(); 77 | LOGGER.error( 78 | "Server said that the endpoint at {} does not exist. Backing off to {} times our poll interval (of {} seconds) to avoid overloading server", 79 | this.target, 80 | maxSkips, 81 | this.intervalLength); 82 | } else if (responseCode == 429) { 83 | increaseSkipCount(); 84 | LOGGER.info( 85 | "RATE LIMITED for the {}. time. Further backing off. Current backoff at {} times our interval (of {} seconds)", 86 | failures.get(), 87 | skips.get(), 88 | this.intervalLength); 89 | } else if (responseCode >= HttpURLConnection.HTTP_INTERNAL_ERROR) { 90 | increaseSkipCount(); 91 | LOGGER.info( 92 | "Server failed with a {} status code. Backing off. Current backoff at {} times our poll interval (of {} seconds)", 93 | responseCode, 94 | skips.get(), 95 | this.intervalLength); 96 | } 97 | } 98 | 99 | public int getSkips() { 100 | return this.skips.get(); 101 | } 102 | 103 | public int getFailures() { 104 | return this.failures.get(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/UnleashFeatureFetcherFactory.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import io.getunleash.repository.FeatureFetcher; 4 | import java.util.function.Function; 5 | 6 | public interface UnleashFeatureFetcherFactory extends Function {} 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/UnleashProperties.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.Properties; 6 | 7 | public class UnleashProperties { 8 | static Properties appProperties; 9 | 10 | static { 11 | try (InputStream is = 12 | UnleashProperties.class.getClassLoader().getResourceAsStream("app.properties")) { 13 | appProperties = new Properties(); 14 | appProperties.load(is); 15 | } catch (IOException ioException) { 16 | appProperties = new Properties(); 17 | appProperties.setProperty("client.specification.version", "4.2.0"); 18 | } 19 | } 20 | 21 | public static String getProperty(String propName) { 22 | return appProperties.getProperty(propName); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/UnleashScheduledExecutor.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import io.getunleash.lang.Nullable; 4 | import java.util.concurrent.Future; 5 | import java.util.concurrent.RejectedExecutionException; 6 | import java.util.concurrent.ScheduledFuture; 7 | 8 | public interface UnleashScheduledExecutor { 9 | @Nullable 10 | ScheduledFuture setInterval(Runnable command, long initialDelaySec, long periodSec) 11 | throws RejectedExecutionException; 12 | 13 | Future scheduleOnce(Runnable runnable); 14 | 15 | public default void shutdown() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/UnleashScheduledExecutorImpl.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import io.getunleash.lang.Nullable; 4 | import java.util.concurrent.*; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public class UnleashScheduledExecutorImpl implements UnleashScheduledExecutor { 9 | 10 | private static final Logger LOG = LoggerFactory.getLogger(UnleashScheduledExecutorImpl.class); 11 | 12 | @Nullable private static UnleashScheduledExecutorImpl INSTANCE; 13 | 14 | private final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor; 15 | private final ExecutorService executorService; 16 | 17 | public UnleashScheduledExecutorImpl() { 18 | ThreadFactory threadFactory = 19 | runnable -> { 20 | Thread thread = Executors.defaultThreadFactory().newThread(runnable); 21 | thread.setName("unleash-api-executor"); 22 | thread.setDaemon(true); 23 | return thread; 24 | }; 25 | 26 | this.scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, threadFactory); 27 | this.scheduledThreadPoolExecutor.setRemoveOnCancelPolicy(true); 28 | 29 | this.executorService = Executors.newSingleThreadExecutor(threadFactory); 30 | } 31 | 32 | public static synchronized UnleashScheduledExecutorImpl getInstance() { 33 | if (INSTANCE == null) { 34 | INSTANCE = new UnleashScheduledExecutorImpl(); 35 | } 36 | return INSTANCE; 37 | } 38 | 39 | @Override 40 | public @Nullable ScheduledFuture setInterval( 41 | Runnable command, long initialDelaySec, long periodSec) { 42 | try { 43 | return scheduledThreadPoolExecutor.scheduleAtFixedRate( 44 | command, initialDelaySec, periodSec, TimeUnit.SECONDS); 45 | } catch (RejectedExecutionException ex) { 46 | LOG.error("Unleash background task crashed", ex); 47 | return null; 48 | } 49 | } 50 | 51 | @Override 52 | public Future scheduleOnce(Runnable runnable) { 53 | return (Future) executorService.submit(runnable); 54 | } 55 | 56 | @Override 57 | public void shutdown() { 58 | this.scheduledThreadPoolExecutor.shutdown(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/UnleashURLs.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import io.getunleash.lang.Nullable; 4 | import java.net.MalformedURLException; 5 | import java.net.URI; 6 | import java.net.URL; 7 | 8 | public class UnleashURLs { 9 | private final URL fetchTogglesURL; 10 | private final URL clientMetricsURL; 11 | private final URL clientRegisterURL; 12 | 13 | public UnleashURLs(URI unleashAPI) { 14 | try { 15 | String unleashAPIstr = unleashAPI.toString(); 16 | fetchTogglesURL = URI.create(unleashAPIstr + "/client/features").normalize().toURL(); 17 | clientMetricsURL = URI.create(unleashAPIstr + "/client/metrics").normalize().toURL(); 18 | clientRegisterURL = URI.create(unleashAPIstr + "/client/register").normalize().toURL(); 19 | 20 | } catch (MalformedURLException ex) { 21 | throw new IllegalArgumentException("Unleash API is not a valid URL: " + unleashAPI); 22 | } 23 | } 24 | 25 | public URL getFetchTogglesURL() { 26 | return fetchTogglesURL; 27 | } 28 | 29 | public URL getClientMetricsURL() { 30 | return clientMetricsURL; 31 | } 32 | 33 | public URL getClientRegisterURL() { 34 | return clientRegisterURL; 35 | } 36 | 37 | public URL getFetchTogglesURL(@Nullable String projectName, @Nullable String namePrefix) { 38 | StringBuilder suffix = new StringBuilder(); 39 | appendParam(suffix, "project", projectName); 40 | appendParam(suffix, "namePrefix", namePrefix); 41 | 42 | try { 43 | return URI.create(fetchTogglesURL + suffix.toString()).normalize().toURL(); 44 | } catch (IllegalArgumentException | MalformedURLException e) { 45 | throw new IllegalArgumentException( 46 | "fetchTogglesURL [" + fetchTogglesURL + suffix + "] was not URL friendly.", e); 47 | } 48 | } 49 | 50 | private void appendParam(StringBuilder suffix, String key, @Nullable String value) { 51 | if (value == null) { 52 | return; 53 | } 54 | if (suffix.length() == 0) { 55 | suffix.append("?"); 56 | } else { 57 | suffix.append("&"); 58 | } 59 | suffix.append(key).append("=").append(value); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/util/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | @NonNullFields 3 | package io.getunleash.util; 4 | 5 | import io.getunleash.lang.NonNullApi; 6 | import io.getunleash.lang.NonNullFields; 7 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/variant/Payload.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.variant; 2 | 3 | import io.getunleash.lang.Nullable; 4 | import java.util.Objects; 5 | 6 | public class Payload { 7 | private String type; 8 | @Nullable private String value; 9 | 10 | public Payload(String type, @Nullable String value) { 11 | this.type = type; 12 | this.value = value; 13 | } 14 | 15 | public String getType() { 16 | return type; 17 | } 18 | 19 | public @Nullable String getValue() { 20 | return value; 21 | } 22 | 23 | @Override 24 | public boolean equals(@Nullable Object o) { 25 | if (this == o) return true; 26 | if (o == null || getClass() != o.getClass()) return false; 27 | Payload payload = (Payload) o; 28 | return Objects.equals(type, payload.type) && Objects.equals(value, payload.value); 29 | } 30 | 31 | @Override 32 | public int hashCode() { 33 | return Objects.hash(type, value); 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "Payload{" + "type='" + type + '\'' + ", value='" + value + '\'' + '}'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/variant/Variant.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.variant; 2 | 3 | import io.getunleash.lang.Nullable; 4 | import java.util.Objects; 5 | import java.util.Optional; 6 | 7 | public class Variant { 8 | public static final Variant DISABLED_VARIANT = new Variant("disabled", (String) null, false); 9 | 10 | private final String name; 11 | @Nullable private final Payload payload; 12 | private final boolean enabled; 13 | @Nullable private final String stickiness; 14 | private final boolean feature_enabled; 15 | 16 | public Variant( 17 | String name, @Nullable Payload payload, boolean enabled, boolean feature_enabled) { 18 | this(name, payload, enabled, null, feature_enabled); 19 | } 20 | 21 | public Variant( 22 | String name, 23 | @Nullable Payload payload, 24 | boolean enabled, 25 | String stickiness, 26 | boolean feature_enabled) { 27 | this.name = name; 28 | this.payload = payload; 29 | this.enabled = enabled; 30 | this.stickiness = stickiness; 31 | this.feature_enabled = feature_enabled; 32 | } 33 | 34 | public Variant(String name, @Nullable String payload, boolean enabled) { 35 | this(name, payload, enabled, null, false); 36 | } 37 | 38 | public Variant( 39 | String name, 40 | @Nullable String payload, 41 | boolean enabled, 42 | String stickiness, 43 | boolean feature_enabled) { 44 | this.name = name; 45 | this.payload = new Payload("string", payload); 46 | this.enabled = enabled; 47 | this.stickiness = stickiness; 48 | this.feature_enabled = feature_enabled; 49 | } 50 | 51 | public String getName() { 52 | return name; 53 | } 54 | 55 | public Optional getPayload() { 56 | return Optional.ofNullable(payload); 57 | } 58 | 59 | public boolean isEnabled() { 60 | return enabled; 61 | } 62 | 63 | public boolean isFeatureEnabled() { 64 | return feature_enabled; 65 | } 66 | 67 | @Nullable 68 | public String getStickiness() { 69 | return stickiness; 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | return "Variant{" 75 | + "name='" 76 | + name 77 | + '\'' 78 | + ", payload='" 79 | + payload 80 | + '\'' 81 | + ", enabled=" 82 | + enabled 83 | + '}'; 84 | } 85 | 86 | @Override 87 | public boolean equals(Object o) { 88 | if (this == o) return true; 89 | if (o == null || getClass() != o.getClass()) return false; 90 | Variant variant = (Variant) o; 91 | return enabled == variant.enabled 92 | && Objects.equals(name, variant.name) 93 | && Objects.equals(payload, variant.payload); 94 | } 95 | 96 | @Override 97 | public int hashCode() { 98 | return Objects.hash(name, payload, enabled); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/io/getunleash/variant/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullFields 2 | @NonNullApi 3 | package io.getunleash.variant; 4 | 5 | import io.getunleash.lang.NonNullApi; 6 | import io.getunleash.lang.NonNullFields; 7 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/io.getunleash/unleash-client-java/reflect-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "io.getunleash.repository.FeatureCollection", 4 | "allDeclaredFields": true, 5 | "allDeclaredConstructors": true, 6 | "unsafeAllocated": true 7 | }, 8 | { 9 | "name": "io.getunleash.repository.ToggleCollection", 10 | "allDeclaredFields": true, 11 | "allDeclaredConstructors": true, 12 | "unsafeAllocated": true 13 | }, 14 | { 15 | "name": "io.getunleash.FeatureToggle", 16 | "allDeclaredFields": true, 17 | "allDeclaredConstructors": true, 18 | "unsafeAllocated": true 19 | }, 20 | { 21 | "name": "io.getunleash.ActivationStrategy", 22 | "allDeclaredFields": true, 23 | "allDeclaredConstructors": true, 24 | "unsafeAllocated": true 25 | }, 26 | { 27 | "name": "io.getunleash.Constraint", 28 | "allDeclaredFields": true, 29 | "allDeclaredConstructors": true, 30 | "unsafeAllocated": true 31 | }, 32 | { 33 | "name": "io.getunleash.variant.VariantDefinition", 34 | "allDeclaredFields": true, 35 | "allDeclaredConstructors": true, 36 | "unsafeAllocated": true 37 | }, 38 | { 39 | "name": "io.getunleash.variant.VariantOverride", 40 | "allDeclaredFields": true, 41 | "allDeclaredConstructors": true, 42 | "unsafeAllocated": true 43 | }, 44 | { 45 | "name": "io.getunleash.repository.SegmentCollection", 46 | "allDeclaredFields": true, 47 | "allDeclaredConstructors": true, 48 | "unsafeAllocated": true 49 | }, 50 | { 51 | "name": "io.getunleash.Segment", 52 | "allDeclaredFields": true, 53 | "allDeclaredConstructors": true, 54 | "unsafeAllocated": true 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/io.getunleash/unleash-client-java/resource-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": [ 3 | { 4 | "pattern": "app\\.properties" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/app.properties: -------------------------------------------------------------------------------- 1 | client.specification.version=${version.unleash.specification} 2 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/RunOnJavaVersions.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | 7 | @Retention(RetentionPolicy.RUNTIME) 8 | @ExtendWith(RunOnJavaVersionsCondition.class) 9 | public @interface RunOnJavaVersions { 10 | String[] javaVersions(); 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/RunOnJavaVersionsCondition.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; 4 | 5 | import java.util.Arrays; 6 | import java.util.Optional; 7 | import org.junit.jupiter.api.extension.ConditionEvaluationResult; 8 | import org.junit.jupiter.api.extension.ExecutionCondition; 9 | import org.junit.jupiter.api.extension.ExtensionContext; 10 | 11 | public class RunOnJavaVersionsCondition implements ExecutionCondition { 12 | @Override 13 | public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext extensionContext) { 14 | Optional annotation = 15 | findAnnotation(extensionContext.getElement(), RunOnJavaVersions.class); 16 | return annotation 17 | .map( 18 | a -> { 19 | String runtimeVersion = getJavaVersion(); 20 | if (runtimeVersion != null && !runtimeVersion.isEmpty()) { 21 | if (Arrays.stream(a.javaVersions()) 22 | .anyMatch(runtimeVersion::startsWith)) { 23 | return ConditionEvaluationResult.enabled( 24 | "Runtime java version is included"); 25 | } else { 26 | return ConditionEvaluationResult.disabled( 27 | "Java version is not among the versions we run this test for"); 28 | } 29 | } else { 30 | return ConditionEvaluationResult.enabled( 31 | "Couldn't find a java version to compare for"); 32 | } 33 | }) 34 | .orElse(ConditionEvaluationResult.enabled("No annotation found")); 35 | } 36 | 37 | private String getJavaVersion() { 38 | return System.getProperty("java.version"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/SynchronousTestExecutor.java: -------------------------------------------------------------------------------- 1 | package io.getunleash; 2 | 3 | import io.getunleash.util.UnleashScheduledExecutor; 4 | import java.util.concurrent.Delayed; 5 | import java.util.concurrent.RejectedExecutionException; 6 | import java.util.concurrent.ScheduledFuture; 7 | import java.util.concurrent.TimeUnit; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class SynchronousTestExecutor implements UnleashScheduledExecutor { 12 | 13 | private static final Logger LOG = LoggerFactory.getLogger(SynchronousTestExecutor.class); 14 | 15 | @Override 16 | public ScheduledFuture setInterval(Runnable command, long initialDelaySec, long periodSec) 17 | throws RejectedExecutionException { 18 | LOG.warn("i will only do this once"); 19 | return scheduleOnce(command); 20 | } 21 | 22 | @Override 23 | public ScheduledFuture scheduleOnce(Runnable runnable) { 24 | runnable.run(); 25 | return new AlreadyCompletedScheduledFuture(); 26 | } 27 | 28 | private static class AlreadyCompletedScheduledFuture implements ScheduledFuture { 29 | @Override 30 | public long getDelay(TimeUnit timeUnit) { 31 | return 0; 32 | } 33 | 34 | @Override 35 | public int compareTo(Delayed delayed) { 36 | return 0; 37 | } 38 | 39 | @Override 40 | public boolean cancel(boolean b) { 41 | return false; 42 | } 43 | 44 | @Override 45 | public boolean isCancelled() { 46 | return false; 47 | } 48 | 49 | @Override 50 | public boolean isDone() { 51 | return false; 52 | } 53 | 54 | @Override 55 | public Void get() { 56 | return null; 57 | } 58 | 59 | @Override 60 | public Void get(long l, TimeUnit timeUnit) { 61 | return null; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/event/ImpressionDataSubscriberTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.Mockito.when; 6 | 7 | import io.getunleash.DefaultUnleash; 8 | import io.getunleash.EngineProxy; 9 | import io.getunleash.SynchronousTestExecutor; 10 | import io.getunleash.Unleash; 11 | import io.getunleash.UnleashContext; 12 | import io.getunleash.engine.VariantDef; 13 | import io.getunleash.util.UnleashConfig; 14 | import java.util.Optional; 15 | import org.junit.jupiter.api.BeforeEach; 16 | import org.junit.jupiter.api.Test; 17 | import org.mockito.Mockito; 18 | 19 | public class ImpressionDataSubscriberTest { 20 | 21 | private ImpressionTestSubscriber testSubscriber = new ImpressionTestSubscriber(); 22 | 23 | private UnleashConfig unleashConfig; 24 | 25 | @BeforeEach 26 | void setup() { 27 | unleashConfig = 28 | new UnleashConfig.Builder() 29 | .appName(SubscriberTest.class.getSimpleName()) 30 | .instanceId(SubscriberTest.class.getSimpleName()) 31 | .unleashAPI("http://localhost:4242/api") 32 | .subscriber(testSubscriber) 33 | .scheduledExecutor(new SynchronousTestExecutor()) 34 | .build(); 35 | } 36 | 37 | @Test 38 | public void noEventsIfImpressionDataIsNotEnabled() { 39 | String featureWithoutImpressionDataEnabled = "feature.with.no.impressionData"; 40 | EngineProxy repo = Mockito.mock(EngineProxy.class); 41 | when(repo.isEnabled(any(String.class), any(UnleashContext.class))).thenReturn(true); 42 | when(repo.shouldEmitImpressionEvent(featureWithoutImpressionDataEnabled)).thenReturn(false); 43 | Unleash unleash = new DefaultUnleash(unleashConfig, repo); 44 | 45 | unleash.isEnabled(featureWithoutImpressionDataEnabled); 46 | assertThat(testSubscriber.isEnabledImpressions).isEqualTo(0); 47 | assertThat(testSubscriber.variantImpressions).isEqualTo(0); 48 | } 49 | 50 | @Test 51 | public void isEnabledEventWhenImpressionDataIsEnabled() { 52 | String featureWithImpressionData = "feature.with.impressionData"; 53 | EngineProxy repo = Mockito.mock(EngineProxy.class); 54 | when(repo.isEnabled(any(String.class), any(UnleashContext.class))).thenReturn(true); 55 | when(repo.shouldEmitImpressionEvent(featureWithImpressionData)).thenReturn(true); 56 | Unleash unleash = new DefaultUnleash(unleashConfig, repo); 57 | 58 | unleash.isEnabled(featureWithImpressionData); 59 | assertThat(testSubscriber.isEnabledImpressions).isEqualTo(1); 60 | assertThat(testSubscriber.variantImpressions).isEqualTo(0); 61 | } 62 | 63 | @Test 64 | public void variantEventWhenVariantIsRequested() { 65 | VariantDef mockVariant = Mockito.mock(VariantDef.class); 66 | String featureWithImpressionData = "feature.with.impressionData"; 67 | EngineProxy repo = Mockito.mock(EngineProxy.class); 68 | when(repo.shouldEmitImpressionEvent(featureWithImpressionData)).thenReturn(true); 69 | when(repo.getVariant(any(String.class), any(UnleashContext.class))) 70 | .thenReturn(Optional.of(mockVariant)); 71 | Unleash unleash = new DefaultUnleash(unleashConfig, repo); 72 | 73 | unleash.getVariant(featureWithImpressionData); 74 | assertThat(testSubscriber.isEnabledImpressions).isEqualTo(0); 75 | assertThat(testSubscriber.variantImpressions).isEqualTo(1); 76 | } 77 | 78 | private class ImpressionTestSubscriber implements UnleashSubscriber { 79 | private int variantImpressions; 80 | private int isEnabledImpressions; 81 | 82 | @Override 83 | public void impression(ImpressionEvent impressionEvent) { 84 | if (impressionEvent instanceof VariantImpressionEvent) { 85 | variantImpressions++; 86 | } else if (impressionEvent instanceof IsEnabledImpressionEvent) { 87 | isEnabledImpressions++; 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/event/SubscriberTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.event; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 4 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 5 | import static io.getunleash.event.ClientFeaturesResponse.Status.CHANGED; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | import com.github.tomakehurst.wiremock.junit5.WireMockExtension; 9 | import io.getunleash.DefaultUnleash; 10 | import io.getunleash.SynchronousTestExecutor; 11 | import io.getunleash.Unleash; 12 | import io.getunleash.UnleashException; 13 | import io.getunleash.metric.ClientRegistration; 14 | import io.getunleash.util.UnleashConfig; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | import org.junit.jupiter.api.extension.RegisterExtension; 20 | 21 | public class SubscriberTest { 22 | 23 | @RegisterExtension 24 | static WireMockExtension serverMock = 25 | WireMockExtension.newInstance() 26 | .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) 27 | .build(); 28 | 29 | private TestSubscriber testSubscriber = new TestSubscriber(); 30 | private UnleashConfig unleashConfig; 31 | 32 | @BeforeEach 33 | void setup() { 34 | unleashConfig = 35 | new UnleashConfig.Builder() 36 | .appName(SubscriberTest.class.getSimpleName()) 37 | .instanceId(SubscriberTest.class.getSimpleName()) 38 | .synchronousFetchOnInitialisation(true) 39 | .unleashAPI("http://localhost:" + serverMock.getPort()) 40 | .subscriber(testSubscriber) 41 | .scheduledExecutor(new SynchronousTestExecutor()) 42 | .build(); 43 | } 44 | 45 | @Test 46 | void subscribersAreNotified() { 47 | serverMock.stubFor(post("/client/register").willReturn(ok())); 48 | serverMock.stubFor(post("/client/metrics").willReturn(ok())); 49 | serverMock.stubFor( 50 | get("/client/features") 51 | .willReturn( 52 | ok().withHeader("Content-Type", "application/json") 53 | .withBody("{\"features\": [], \"version\": 2 }"))); 54 | Unleash unleash = new DefaultUnleash(unleashConfig); 55 | 56 | unleash.isEnabled("myFeature"); 57 | unleash.isEnabled("myFeature"); 58 | unleash.isEnabled("myFeature"); 59 | 60 | assertThat(testSubscriber.togglesFetchedCounter) 61 | .isEqualTo(2); // Server successfully returns, we call synchronous fetch and 62 | // schedule 63 | // once, so 2 calls. 64 | assertThat(testSubscriber.status).isEqualTo(CHANGED); 65 | assertThat(testSubscriber.toggleEvaluatedCounter).isEqualTo(3); 66 | assertThat(testSubscriber.toggleName).isEqualTo("myFeature"); 67 | assertThat(testSubscriber.toggleEnabled).isFalse(); 68 | assertThat(testSubscriber.errors).isEmpty(); 69 | 70 | // assertThat(testSubscriber.events).filteredOn(e -> e instanceof TogglesBootstrapped) 71 | // .hasSize(1); 72 | assertThat(testSubscriber.events).filteredOn(e -> e instanceof UnleashReady).hasSize(1); 73 | assertThat(testSubscriber.events).filteredOn(e -> e instanceof ToggleEvaluated).hasSize(3); 74 | assertThat(testSubscriber.events) 75 | .filteredOn(e -> e instanceof ClientFeaturesResponse) 76 | .hasSize(2); 77 | assertThat(testSubscriber.events) 78 | .filteredOn(e -> e instanceof ClientRegistration) 79 | .hasSize(1); 80 | } 81 | 82 | private class TestSubscriber implements UnleashSubscriber { 83 | 84 | private int togglesFetchedCounter; 85 | private ClientFeaturesResponse.Status status; 86 | 87 | private int toggleEvaluatedCounter; 88 | private String toggleName; 89 | private boolean toggleEnabled; 90 | 91 | private List events = new ArrayList<>(); 92 | private List errors = new ArrayList<>(); 93 | 94 | @Override 95 | public void on(UnleashEvent unleashEvent) { 96 | this.events.add(unleashEvent); 97 | } 98 | 99 | @Override 100 | public void onError(UnleashException unleashException) { 101 | this.errors.add(unleashException); 102 | } 103 | 104 | @Override 105 | public void toggleEvaluated(ToggleEvaluated toggleEvaluated) { 106 | this.toggleEvaluatedCounter++; 107 | this.toggleName = toggleEvaluated.getToggleName(); 108 | this.toggleEnabled = toggleEvaluated.isEnabled(); 109 | } 110 | 111 | @Override 112 | public void togglesFetched(ClientFeaturesResponse toggleResponse) { 113 | this.togglesFetchedCounter++; 114 | this.status = toggleResponse.getStatus(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/example/CustomStrategy.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.example; 2 | 3 | import io.getunleash.UnleashContext; 4 | import io.getunleash.strategy.Strategy; 5 | import java.util.Map; 6 | 7 | final class CustomStrategy implements Strategy { 8 | @Override 9 | public String getName() { 10 | return "custom"; 11 | } 12 | 13 | @Override 14 | public boolean isEnabled(Map parameters, UnleashContext context) { 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/example/UnleashUsageTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.example; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | 5 | import io.getunleash.DefaultUnleash; 6 | import io.getunleash.Unleash; 7 | import io.getunleash.util.UnleashConfig; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class UnleashUsageTest { 11 | 12 | @Test 13 | public void wire() { 14 | UnleashConfig config = 15 | new UnleashConfig.Builder() 16 | .appName("test") 17 | .instanceId("my-hostname:6517") 18 | .synchronousFetchOnInitialisation(false) 19 | .unleashAPI("http://localhost:4242/api") 20 | .build(); 21 | 22 | Unleash unleash = new DefaultUnleash(config, new CustomStrategy()); 23 | 24 | assertFalse(unleash.isEnabled("myFeature")); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/integration/TestCase.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.integration; 2 | 3 | public class TestCase { 4 | private String description; 5 | private UnleashContextDefinition context; 6 | private String toggleName; 7 | private boolean expectedResult; 8 | 9 | public UnleashContextDefinition getContext() { 10 | return context; 11 | } 12 | 13 | public String getDescription() { 14 | return description; 15 | } 16 | 17 | public String getToggleName() { 18 | return toggleName; 19 | } 20 | 21 | public boolean getExpectedResult() { 22 | return expectedResult; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/integration/TestCaseVariant.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.integration; 2 | 3 | import io.getunleash.variant.Variant; 4 | 5 | public class TestCaseVariant { 6 | private String description; 7 | private UnleashContextDefinition context; 8 | private String toggleName; 9 | private Variant expectedResult; 10 | 11 | public UnleashContextDefinition getContext() { 12 | return context; 13 | } 14 | 15 | public String getDescription() { 16 | return description; 17 | } 18 | 19 | public String getToggleName() { 20 | return toggleName; 21 | } 22 | 23 | public Variant getExpectedResult() { 24 | if (expectedResult.getName().equals("disabled")) { 25 | Variant clone = 26 | new Variant( 27 | Variant.DISABLED_VARIANT.getName(), 28 | Variant.DISABLED_VARIANT.getPayload().orElse(null), 29 | Variant.DISABLED_VARIANT.isEnabled(), 30 | Variant.DISABLED_VARIANT.getStickiness(), 31 | expectedResult.isFeatureEnabled()); 32 | return clone; 33 | } 34 | 35 | return expectedResult; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/integration/TestDefinition.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.integration; 2 | 3 | import com.google.gson.JsonObject; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class TestDefinition { 8 | private String name; 9 | private JsonObject state; 10 | private List tests = new ArrayList<>(); 11 | private List variantTests = new ArrayList<>(); 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public JsonObject getState() { 18 | return state; 19 | } 20 | 21 | public List getTests() { 22 | return tests; 23 | } 24 | 25 | public List getVariantTests() { 26 | return variantTests; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/integration/UnleashContextDefinition.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.integration; 2 | 3 | import java.util.Map; 4 | 5 | public class UnleashContextDefinition { 6 | private final String userId; 7 | private final String sessionId; 8 | private final String remoteAddress; 9 | private final String environment; 10 | private final String appName; 11 | private final String currentTime; 12 | 13 | // Custom context fields used in tests 14 | private final Map properties; 15 | 16 | public UnleashContextDefinition( 17 | String userId, 18 | String sessionId, 19 | String remoteAddress, 20 | String environment, 21 | String appName, 22 | String currentTime, 23 | Map properties) { 24 | this.userId = userId; 25 | this.sessionId = sessionId; 26 | this.remoteAddress = remoteAddress; 27 | this.environment = environment; 28 | this.appName = appName; 29 | this.currentTime = currentTime; 30 | this.properties = properties; 31 | } 32 | 33 | public String getUserId() { 34 | return userId; 35 | } 36 | 37 | public String getSessionId() { 38 | return sessionId; 39 | } 40 | 41 | public String getRemoteAddress() { 42 | return remoteAddress; 43 | } 44 | 45 | public String getEnvironment() { 46 | return environment; 47 | } 48 | 49 | public String getAppName() { 50 | return appName; 51 | } 52 | 53 | public String getCurrentTime() { 54 | return currentTime; 55 | } 56 | 57 | public Map getProperties() { 58 | return properties; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/repository/FeatureBackupHandlerFileTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import io.getunleash.FeatureDefinition; 7 | import io.getunleash.util.ClientFeaturesParser; 8 | import io.getunleash.util.UnleashConfig; 9 | import java.io.File; 10 | import java.net.URISyntaxException; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import org.junit.jupiter.api.Test; 14 | 15 | public class FeatureBackupHandlerFileTest { 16 | 17 | @Test 18 | public void test_read() { 19 | UnleashConfig config = 20 | UnleashConfig.builder() 21 | .appName("test") 22 | .unleashAPI("http://http://unleash.org") 23 | .backupFile(getClass().getResource("/unleash-repo-v2.json").getFile()) 24 | .build(); 25 | FeatureBackupHandlerFile backupHandler = new FeatureBackupHandlerFile(config); 26 | String clientFeatureJson = backupHandler.read().get(); 27 | 28 | List featureCollection = ClientFeaturesParser.parse(clientFeatureJson); 29 | Optional feature = 30 | featureCollection.stream().filter(f -> f.getName().equals("featureX")).findFirst(); 31 | 32 | assertThat(feature).isNotEmpty(); 33 | } 34 | 35 | @Test 36 | public void test_read_file_with_invalid_data() throws Exception { 37 | UnleashConfig config = 38 | UnleashConfig.builder() 39 | .appName("test") 40 | .unleashAPI("http://unleash.org") 41 | .backupFile( 42 | getClass() 43 | .getResource("/unleash-repo-without-feature-field.json") 44 | .getFile()) 45 | .build(); 46 | 47 | FeatureBackupHandlerFile fileGivingNullFeature = new FeatureBackupHandlerFile(config); 48 | assertNotNull(fileGivingNullFeature.read()); 49 | } 50 | 51 | @Test 52 | public void test_read_without_file() throws URISyntaxException { 53 | UnleashConfig config = 54 | UnleashConfig.builder() 55 | .appName("test") 56 | .unleashAPI("http://unleash.org") 57 | .backupFile("/does/not/exist.json") 58 | .build(); 59 | 60 | FeatureBackupHandlerFile backupHandler = new FeatureBackupHandlerFile(config); 61 | Optional featureCollection = backupHandler.read(); 62 | 63 | assertThat(featureCollection).isEmpty(); 64 | } 65 | 66 | @Test 67 | public void test_read_write_is_symmetrical() throws InterruptedException { 68 | String backupFile = 69 | System.getProperty("java.io.tmpdir") 70 | + File.separatorChar 71 | + "unleash-repo-v2-test-write.json"; 72 | UnleashConfig config = 73 | UnleashConfig.builder() 74 | .appName("test") 75 | .unleashAPI("http://unleash.org") 76 | .backupFile(backupFile) 77 | .build(); 78 | 79 | String staticData = 80 | "{\"version\":2,\"segments\":[{\"id\":1,\"name\":\"some-name\",\"description\":null,\"constraints\":[{\"contextName\":\"some-name\",\"operator\":\"IN\",\"value\":\"name\",\"inverted\":false,\"caseInsensitive\":true}]}],\"features\":[{\"name\":\"Test.variants\",\"description\":null,\"enabled\":true,\"strategies\":[{\"name\":\"default\",\"segments\":[1]}],\"variants\":[{\"name\":\"variant1\",\"weight\":50},{\"name\":\"variant2\",\"weight\":50}],\"createdAt\":\"2019-01-24T10:41:45.236Z\"}]}"; 81 | 82 | FeatureBackupHandlerFile backupHandler = new FeatureBackupHandlerFile(config); 83 | backupHandler.write(staticData); 84 | backupHandler = new FeatureBackupHandlerFile(config); 85 | Optional features = backupHandler.read(); 86 | assertEquals(staticData, features.get()); 87 | } 88 | 89 | @Test 90 | public void test_file_is_directory_should_not_crash() { 91 | String backupFileIsDir = System.getProperty("java.io.tmpdir"); 92 | UnleashConfig config = 93 | UnleashConfig.builder() 94 | .appName("test") 95 | .unleashAPI("http://unleash.org") 96 | .backupFile(backupFileIsDir) 97 | .build(); 98 | 99 | String staticData = 100 | "{\"version\":2,\"segments\":[{\"id\":1,\"name\":\"some-name\",\"description\":null,\"constraints\":[{\"contextName\":\"some-name\",\"operator\":\"IN\",\"value\":\"name\",\"inverted\":false,\"caseInsensitive\":true}]}],\"features\":[{\"name\":\"Test.variants\",\"description\":null,\"enabled\":true,\"strategies\":[{\"name\":\"default\",\"segments\":[1]}],\"variants\":[{\"name\":\"variant1\",\"weight\":50},{\"name\":\"variant2\",\"weight\":50}],\"createdAt\":\"2019-01-24T10:41:45.236Z\"}]}"; 101 | 102 | FeatureBackupHandlerFile backupHandler = new FeatureBackupHandlerFile(config); 103 | 104 | backupHandler.write(staticData); 105 | assertTrue(true, "Did not crash even if backup-writer yields IOException"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/repository/ToggleBootstrapFileProviderTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.io.File; 6 | import java.util.Optional; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class ToggleBootstrapFileProviderTest { 10 | 11 | @Test 12 | public void shouldBeAbleToLoadFilePassedInAsArgument() { 13 | File exampleRepoFile = 14 | new File(getClass().getClassLoader().getResource("unleash-repo-v0.json").getFile()); 15 | ToggleBootstrapFileProvider toggleBootstrapFileProvider = 16 | new ToggleBootstrapFileProvider(exampleRepoFile.getAbsolutePath()); 17 | assertThat(toggleBootstrapFileProvider.read()).isNotEmpty(); 18 | } 19 | 20 | @Test 21 | public void shouldBeAbleToLoadFilePassedInEnvironment() { 22 | File exampleRepoFile = 23 | new File(getClass().getClassLoader().getResource("unleash-repo-v0.json").getFile()); 24 | System.setProperty("UNLEASH_BOOTSTRAP_FILE", exampleRepoFile.getAbsolutePath()); 25 | ToggleBootstrapFileProvider toggleBootstrapFileProvider = new ToggleBootstrapFileProvider(); 26 | assertThat(toggleBootstrapFileProvider.read()).isNotEmpty(); 27 | } 28 | 29 | @Test 30 | public void shouldBeAbleToLoadfileFromClasspathReference() { 31 | System.setProperty("UNLEASH_BOOTSTRAP_FILE", "classpath:unleash-repo-v0.json"); 32 | ToggleBootstrapFileProvider bootstrap = new ToggleBootstrapFileProvider(); 33 | 34 | Optional read = bootstrap.read(); 35 | assertThat(read).isPresent(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/repository/UnleashExceptionExtension.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.repository; 2 | 3 | import io.getunleash.UnleashException; 4 | import org.junit.jupiter.api.extension.ExtensionContext; 5 | import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; 6 | 7 | public class UnleashExceptionExtension implements TestExecutionExceptionHandler { 8 | @Override 9 | public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) 10 | throws Throwable { 11 | if (throwable instanceof UnleashException) { 12 | return; 13 | } 14 | throw throwable; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/util/ClientFeaturesParserTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import io.getunleash.FeatureDefinition; 7 | import java.util.List; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class ClientFeaturesParserTest { 11 | 12 | @Test 13 | public void test_basic_parse() { 14 | String basicFeatures = 15 | "{\"features\":[{\"name\":\"featureX\",\"project\":\"default\",\"enabled\":true,\"strategies\":[{\"name\":\"default\"}]}]}"; 16 | List parsed = ClientFeaturesParser.parse(basicFeatures); 17 | 18 | assertEquals(1, parsed.size()); 19 | 20 | FeatureDefinition feature = parsed.get(0); 21 | 22 | assertEquals(feature.getName(), "featureX"); 23 | assertEquals(feature.getProject(), "default"); 24 | assertThat(feature.getType()).isEmpty(); 25 | } 26 | 27 | @Test 28 | public void test_project_is_null_if_not_in_original() { 29 | String basicFeatures = 30 | "{\"features\":[{\"name\":\"featureX\",\"enabled\":true,\"strategies\":[{\"name\":\"default\"}]}]}"; 31 | List parsed = ClientFeaturesParser.parse(basicFeatures); 32 | 33 | assertEquals(1, parsed.size()); 34 | 35 | FeatureDefinition feature = parsed.get(0); 36 | 37 | assertEquals(feature.getName(), "featureX"); 38 | assertEquals(feature.getProject(), null); 39 | } 40 | 41 | @Test 42 | public void test_type_is_set_if_present() { 43 | String basicFeatures = 44 | "{\"features\":[{\"name\":\"featureX\",\"type\":\"experiment\",\"enabled\":true,\"strategies\":[{\"name\":\"default\"}]}]}"; 45 | List parsed = ClientFeaturesParser.parse(basicFeatures); 46 | 47 | assertEquals(1, parsed.size()); 48 | 49 | FeatureDefinition feature = parsed.get(0); 50 | 51 | assertEquals(feature.getName(), "featureX"); 52 | assertThat(feature.getType()).isNotEmpty(); 53 | assertEquals(feature.getType().get(), "experiment"); 54 | } 55 | 56 | @Test 57 | public void test_deserialize_fails_if_name_is_not_set() { 58 | String basicFeatures = 59 | "{\"features\":[{\"project\":\"default\",\"enabled\":true,\"strategies\":[{\"name\":\"default\"}]}]}"; 60 | 61 | try { 62 | ClientFeaturesParser.parse(basicFeatures); 63 | assertThat(false).isTrue(); 64 | } catch (Exception e) { 65 | assertThat(e).isInstanceOf(RuntimeException.class); 66 | assertThat(e.getMessage()).contains("Missing required field 'name'"); 67 | } 68 | } 69 | 70 | @Test 71 | public void test_enabled_property_returned_if_set() { 72 | String basicFeatures = 73 | "{\"features\":[{\"name\":\"featureX\",\"project\":\"default\",\"enabled\":true,\"strategies\":[{\"name\":\"default\"}]}]}"; 74 | List parsed = ClientFeaturesParser.parse(basicFeatures); 75 | 76 | assertEquals(1, parsed.size()); 77 | 78 | FeatureDefinition feature = parsed.get(0); 79 | 80 | assertEquals(feature.getName(), "featureX"); 81 | assertEquals(feature.getProject(), "default"); 82 | assertEquals(feature.environmentEnabled(), true); 83 | } 84 | 85 | @Test 86 | public void test_enabled_property_defaults_to_false() { 87 | String basicFeatures = 88 | "{\"features\":[{\"name\":\"featureX\",\"project\":\"default\",\"strategies\":[{\"name\":\"default\"}]}]}"; 89 | List parsed = ClientFeaturesParser.parse(basicFeatures); 90 | 91 | assertEquals(1, parsed.size()); 92 | 93 | FeatureDefinition feature = parsed.get(0); 94 | 95 | assertEquals(feature.getName(), "featureX"); 96 | assertEquals(feature.getProject(), "default"); 97 | assertEquals(feature.environmentEnabled(), false); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/util/ThrottlerTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.net.MalformedURLException; 6 | import java.net.URI; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class ThrottlerTest { 10 | 11 | @Test 12 | public void shouldNeverDecrementFailuresOrSkipsBelowZero() throws MalformedURLException { 13 | Throttler throttler = 14 | new Throttler(10, 300, URI.create("https://localhost:1500/api").toURL()); 15 | throttler.decrementFailureCountAndResetSkips(); 16 | throttler.decrementFailureCountAndResetSkips(); 17 | throttler.decrementFailureCountAndResetSkips(); 18 | throttler.decrementFailureCountAndResetSkips(); 19 | throttler.decrementFailureCountAndResetSkips(); 20 | assertThat(throttler.getSkips()).isEqualTo(0); 21 | assertThat(throttler.getFailures()).isEqualTo(0); 22 | } 23 | 24 | @Test 25 | public void setToMaxShouldReduceDownEventually() throws MalformedURLException { 26 | Throttler throttler = 27 | new Throttler(150, 300, URI.create("https://localhost:1500/api").toURL()); 28 | throttler.handleHttpErrorCodes(404); 29 | assertThat(throttler.getSkips()).isEqualTo(2); 30 | assertThat(throttler.getFailures()).isEqualTo(1); 31 | throttler.skipped(); 32 | assertThat(throttler.getSkips()).isEqualTo(1); 33 | assertThat(throttler.getFailures()).isEqualTo(1); 34 | throttler.skipped(); 35 | assertThat(throttler.getSkips()).isEqualTo(0); 36 | assertThat(throttler.getFailures()).isEqualTo(1); 37 | throttler.decrementFailureCountAndResetSkips(); 38 | assertThat(throttler.getSkips()).isEqualTo(0); 39 | assertThat(throttler.getFailures()).isEqualTo(0); 40 | throttler.decrementFailureCountAndResetSkips(); 41 | assertThat(throttler.getSkips()).isEqualTo(0); 42 | assertThat(throttler.getFailures()).isEqualTo(0); 43 | } 44 | 45 | @Test 46 | public void handleIntermittentFailures() throws MalformedURLException { 47 | Throttler throttler = 48 | new Throttler(50, 300, URI.create("https://localhost:1500/api").toURL()); 49 | throttler.handleHttpErrorCodes(429); 50 | throttler.handleHttpErrorCodes(429); 51 | throttler.handleHttpErrorCodes(503); 52 | throttler.handleHttpErrorCodes(429); 53 | assertThat(throttler.getSkips()).isEqualTo(4); 54 | assertThat(throttler.getFailures()).isEqualTo(4); 55 | throttler.decrementFailureCountAndResetSkips(); 56 | assertThat(throttler.getSkips()).isEqualTo(3); 57 | assertThat(throttler.getFailures()).isEqualTo(3); 58 | throttler.handleHttpErrorCodes(429); 59 | assertThat(throttler.getSkips()).isEqualTo(4); 60 | assertThat(throttler.getFailures()).isEqualTo(4); 61 | throttler.decrementFailureCountAndResetSkips(); 62 | throttler.decrementFailureCountAndResetSkips(); 63 | throttler.decrementFailureCountAndResetSkips(); 64 | throttler.decrementFailureCountAndResetSkips(); 65 | throttler.decrementFailureCountAndResetSkips(); 66 | throttler.decrementFailureCountAndResetSkips(); 67 | throttler.decrementFailureCountAndResetSkips(); 68 | assertThat(throttler.getSkips()).isEqualTo(0); 69 | assertThat(throttler.getFailures()).isEqualTo(0); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/util/UnleashScheduledExecutorImplTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class UnleashScheduledExecutorImplTest { 9 | 10 | private UnleashScheduledExecutorImpl unleashScheduledExecutor = 11 | new UnleashScheduledExecutorImpl(); 12 | private int periodicalTaskCounter; 13 | 14 | @BeforeEach 15 | public void setup() { 16 | this.periodicalTaskCounter = 0; 17 | } 18 | 19 | @Test 20 | public void scheduleOnce_doNotInterfereWithPeriodicalTasks() { 21 | unleashScheduledExecutor.setInterval(this::periodicalTask, 0, 1); 22 | unleashScheduledExecutor.scheduleOnce(this::sleep5seconds); 23 | sleep5seconds(); 24 | assertThat(periodicalTaskCounter).isGreaterThan(3); 25 | } 26 | 27 | private void sleep5seconds() { 28 | try { 29 | Thread.sleep(5_000); 30 | } catch (InterruptedException e) { 31 | e.printStackTrace(); 32 | } 33 | } 34 | 35 | private void periodicalTask() { 36 | this.periodicalTaskCounter++; 37 | } 38 | 39 | @Test 40 | public void shutdown_stopsRunningScheduledTasks() { 41 | unleashScheduledExecutor.setInterval(this::periodicalTask, 5, 1); 42 | unleashScheduledExecutor.shutdown(); 43 | sleep5seconds(); 44 | assertThat(periodicalTaskCounter).isEqualTo(0); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/io/getunleash/util/UnleashURLsTest.java: -------------------------------------------------------------------------------- 1 | package io.getunleash.util; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import java.net.URI; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class UnleashURLsTest { 10 | 11 | @Test 12 | public void should_handle_additional_slash() { 13 | UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/")); 14 | assertThat(urls.getFetchTogglesURL().toString()) 15 | .isEqualTo("http://unleash.com/api/client/features"); 16 | } 17 | 18 | @Test 19 | public void should_set_correct_client_register_url() { 20 | UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/")); 21 | assertThat(urls.getClientRegisterURL().toString()) 22 | .isEqualTo("http://unleash.com/api/client/register"); 23 | } 24 | 25 | @Test 26 | public void should_set_correct_client_metrics_url() { 27 | UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/")); 28 | assertThat(urls.getClientMetricsURL().toString()) 29 | .isEqualTo("http://unleash.com/api/client/metrics"); 30 | } 31 | 32 | @Test 33 | public void should_set_correct_fetch_url() { 34 | UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/")); 35 | assertThat(urls.getFetchTogglesURL().toString()) 36 | .isEqualTo("http://unleash.com/api/client/features"); 37 | } 38 | 39 | @Test 40 | public void should_set_build_fetch_url_if_project_and_prefix_are_null() { 41 | UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/")); 42 | assertThat(urls.getFetchTogglesURL(null, null).toString()) 43 | .isEqualTo("http://unleash.com/api/client/features"); 44 | } 45 | 46 | @Test 47 | public void should_set_build_fetch_url_with_project() { 48 | UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/")); 49 | assertThat(urls.getFetchTogglesURL("myProject", null).toString()) 50 | .isEqualTo("http://unleash.com/api/client/features?project=myProject"); 51 | } 52 | 53 | @Test 54 | public void should_set_build_fetch_url_with_nameprefix() { 55 | UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/")); 56 | assertThat(urls.getFetchTogglesURL(null, "prefix.").toString()) 57 | .isEqualTo("http://unleash.com/api/client/features?namePrefix=prefix."); 58 | } 59 | 60 | @Test 61 | public void should_set_build_fetch_url_with_project_and_nameprefix() { 62 | UnleashURLs urls = new UnleashURLs(URI.create("http://unleash.com/api/")); 63 | assertThat(urls.getFetchTogglesURL("aproject", "prefix.").toString()) 64 | .isEqualTo( 65 | "http://unleash.com/api/client/features?project=aproject&namePrefix=prefix."); 66 | } 67 | 68 | @Test() 69 | public void should_throw() { 70 | assertThrows( 71 | IllegalArgumentException.class, () -> new UnleashURLs(URI.create("unleash.com"))); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/resources/__files/features-v0.json: -------------------------------------------------------------------------------- 1 | {"features": [ 2 | { 3 | "name": "featureX", 4 | "enabled": true, 5 | "strategy": "default" 6 | }, 7 | { 8 | "name": "featureY", 9 | "enabled": false, 10 | "strategy": "baz", 11 | "parameters": { 12 | "foo": "bar" 13 | } 14 | }, 15 | { 16 | "name": "featureZ", 17 | "enabled": true, 18 | "strategy": "baz", 19 | "parameters": { 20 | "foo": "rab" 21 | } 22 | } 23 | ]} -------------------------------------------------------------------------------- /src/test/resources/__files/features-v1-with-variants.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "features": [ 4 | { 5 | "name": "Test.old", 6 | "description": "No variants here!", 7 | "enabled": true, 8 | "strategies": [ 9 | { 10 | "name": "default" 11 | } 12 | ], 13 | "variants": null, 14 | "createdAt": "2019-01-24T10:38:10.370Z" 15 | }, 16 | { 17 | "name": "Test.variants", 18 | "description": null, 19 | "enabled": true, 20 | "strategies": [ 21 | { 22 | "name": "default" 23 | } 24 | ], 25 | "variants": [ 26 | { 27 | "name": "variant1", 28 | "weight": 50 29 | }, 30 | { 31 | "name": "variant2", 32 | "weight": 50 33 | } 34 | ], 35 | "createdAt": "2019-01-24T10:41:45.236Z" 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /src/test/resources/__files/features-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "features": [ 4 | { 5 | "name": "featureX", 6 | "enabled": true, 7 | "strategies": [ 8 | { 9 | "name": "default" 10 | } 11 | 12 | ] 13 | }, 14 | { 15 | "name": "featureY", 16 | "enabled": false, 17 | "strategies": [ 18 | { 19 | "name": "baz", 20 | "parameters": { 21 | "foo": "bar" 22 | } 23 | } 24 | 25 | ] 26 | }, 27 | { 28 | "name": "featureZ", 29 | "enabled": true, 30 | "strategies": [ 31 | { 32 | "name": "baz", 33 | "parameters": { 34 | "foo": "rab" 35 | } 36 | } 37 | 38 | ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /src/test/resources/__files/features-v2-with-segments.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "segments": [ 4 | { 5 | "id": 1, 6 | "name": "some-name", 7 | "description": null, 8 | "constraints": [ 9 | { 10 | "contextName": "some-name", 11 | "operator": "IN", 12 | "value": "name", 13 | "inverted": false, 14 | "caseInsensitive": true 15 | } 16 | ] 17 | } 18 | ], 19 | "features": [ 20 | { 21 | "name": "Test.old", 22 | "description": "No variants here!", 23 | "enabled": true, 24 | "strategies": [ 25 | { 26 | "name": "default" 27 | } 28 | ], 29 | "variants": null, 30 | "createdAt": "2019-01-24T10:38:10.370Z" 31 | }, 32 | { 33 | "name": "Test.variants", 34 | "description": null, 35 | "enabled": true, 36 | "strategies": [ 37 | { 38 | "name": "default", 39 | "segments": [ 40 | 1 41 | ] 42 | } 43 | ], 44 | "variants": [ 45 | { 46 | "name": "variant1", 47 | "weight": 50 48 | }, 49 | { 50 | "name": "variant2", 51 | "weight": 50 52 | } 53 | ], 54 | "createdAt": "2019-01-24T10:41:45.236Z" 55 | }, 56 | { 57 | "name": "featureX", 58 | "enabled": true, 59 | "strategies": [ 60 | { 61 | "name": "default" 62 | } 63 | ] 64 | }, 65 | { 66 | "name": "featureY", 67 | "enabled": false, 68 | "strategies": [ 69 | { 70 | "name": "baz", 71 | "parameters": { 72 | "foo": "bar" 73 | } 74 | } 75 | ] 76 | 77 | }, 78 | { 79 | "name": "featureZ", 80 | "enabled": true, 81 | "strategies": [ 82 | { 83 | "name": "default" 84 | }, 85 | { 86 | "name": "hola", 87 | "parameters": { 88 | "name": "val" 89 | }, 90 | "segments": [1] 91 | } 92 | ] 93 | 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /src/test/resources/empty-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1 3 | } -------------------------------------------------------------------------------- /src/test/resources/empty.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unleash/unleash-client-java/fb576bfd7d68cfd27d4a32109a9735a197659f5b/src/test/resources/empty.json -------------------------------------------------------------------------------- /src/test/resources/features-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": [ 3 | { 4 | "name": "featureX", 5 | "enabled": true, 6 | "strategy": "default" 7 | }, 8 | { 9 | "name": "featureY", 10 | "enabled": false, 11 | "strategy": "baz", 12 | "parameters": { 13 | "foo": "bar" 14 | } 15 | }, 16 | { 17 | "name": "featureZ", 18 | "enabled": true, 19 | "strategy": "baz", 20 | "parameters": { 21 | "foo": "rab" 22 | } 23 | } 24 | ]} -------------------------------------------------------------------------------- /src/test/resources/features-v1-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "features": [] 4 | } -------------------------------------------------------------------------------- /src/test/resources/features-v1-with-variants.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "features": [ 4 | { 5 | "name": "Test.old", 6 | "description": "No variants here!", 7 | "enabled": true, 8 | "strategies": [ 9 | { 10 | "name": "default" 11 | } 12 | ], 13 | "variants": null, 14 | "createdAt": "2019-01-24T10:38:10.370Z" 15 | }, 16 | { 17 | "name": "Test.variants", 18 | "description": null, 19 | "enabled": true, 20 | "strategies": [ 21 | { 22 | "name": "default" 23 | } 24 | ], 25 | "variants": [ 26 | { 27 | "name": "variant1", 28 | "weight": 50 29 | }, 30 | { 31 | "name": "variant2", 32 | "weight": 50 33 | } 34 | ], 35 | "createdAt": "2019-01-24T10:41:45.236Z" 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /src/test/resources/features-v1.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "version": 1, 4 | "features": [ 5 | { 6 | "name": "featureX", 7 | "enabled": true, 8 | "strategies": [ 9 | { 10 | "name": "default" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "featureY", 16 | "enabled": false, 17 | "strategies": [ 18 | { 19 | "name": "baz", 20 | "parameters": { 21 | "foo": "bar" 22 | } 23 | } 24 | ] 25 | 26 | }, 27 | { 28 | "name": "featureZ", 29 | "enabled": true, 30 | "strategies": [ 31 | { 32 | "name": "default" 33 | }, 34 | { 35 | "name": "hola", 36 | "parameters": { 37 | "name": "val" 38 | } 39 | } 40 | ] 41 | 42 | } 43 | ]} -------------------------------------------------------------------------------- /src/test/resources/features-v2-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "segments": [], 4 | "features": [] 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/features-v2-with-segments.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "segments": [ 4 | { 5 | "id": 1, 6 | "name": "some-name", 7 | "description": null, 8 | "constraints": [ 9 | { 10 | "contextName": "some-name", 11 | "operator": "IN", 12 | "value": "name", 13 | "inverted": false, 14 | "caseInsensitive": true 15 | } 16 | ] 17 | } 18 | ], 19 | "features": [ 20 | { 21 | "name": "Test.old", 22 | "description": "No variants here!", 23 | "enabled": true, 24 | "strategies": [ 25 | { 26 | "name": "default" 27 | } 28 | ], 29 | "variants": null, 30 | "createdAt": "2019-01-24T10:38:10.370Z" 31 | }, 32 | { 33 | "name": "Test.variants", 34 | "description": null, 35 | "enabled": true, 36 | "strategies": [ 37 | { 38 | "name": "default", 39 | "segments": [ 40 | 1 41 | ] 42 | } 43 | ], 44 | "variants": [ 45 | { 46 | "name": "variant1", 47 | "weight": 50 48 | }, 49 | { 50 | "name": "variant2", 51 | "weight": 50 52 | } 53 | ], 54 | "createdAt": "2019-01-24T10:41:45.236Z" 55 | }, 56 | { 57 | "name": "featureX", 58 | "enabled": true, 59 | "strategies": [ 60 | { 61 | "name": "default" 62 | } 63 | ] 64 | }, 65 | { 66 | "name": "featureY", 67 | "enabled": false, 68 | "strategies": [ 69 | { 70 | "name": "baz", 71 | "parameters": { 72 | "foo": "bar" 73 | } 74 | } 75 | ] 76 | 77 | }, 78 | { 79 | "name": "featureZ", 80 | "enabled": true, 81 | "strategies": [ 82 | { 83 | "name": "default" 84 | }, 85 | { 86 | "name": "hola", 87 | "parameters": { 88 | "name": "val" 89 | }, 90 | "segments": [1] 91 | } 92 | ] 93 | 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/resources/unleash-repo-v0.json: -------------------------------------------------------------------------------- 1 | {"features": [ 2 | { 3 | "name": "presentFeature", 4 | "enabled": true, 5 | "strategy": "default" 6 | }, 7 | { 8 | "name": "enabledFeature", 9 | "enabled": true, 10 | "strategy": "default" 11 | }, 12 | { 13 | "name": "disabledFeature", 14 | "enabled": false, 15 | "strategy": "default" 16 | }, 17 | { 18 | "name": "featureCustomStrategy", 19 | "enabled": false, 20 | "strategy": "someCustomStrategy", 21 | "parameters": { 22 | "customParameter": "customValue" 23 | } 24 | } 25 | ]} -------------------------------------------------------------------------------- /src/test/resources/unleash-repo-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "features": [ 4 | { 5 | "name": "featureX", 6 | "enabled": true, 7 | "strategies": [ 8 | { 9 | "name": "default" 10 | } 11 | ] 12 | }, 13 | { 14 | "name": "featureY", 15 | "enabled": false, 16 | "strategies": [ 17 | { 18 | "name": "baz", 19 | "parameters": { 20 | "foo": "bar" 21 | } 22 | } 23 | ] 24 | 25 | }, 26 | { 27 | "name": "featureZ", 28 | "enabled": true, 29 | "strategies": [ 30 | { 31 | "name": "default" 32 | }, 33 | { 34 | "name": "hola", 35 | "parameters": { 36 | "name": "val" 37 | } 38 | } 39 | ] 40 | 41 | } 42 | ]} -------------------------------------------------------------------------------- /src/test/resources/unleash-repo-v2-advanced.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "segments": [ 4 | { 5 | "id": 0, 6 | "name": "hasEnoughWins", 7 | "description": null, 8 | "constraints": [ 9 | { 10 | "contextName": "wins", 11 | "operator": "NUM_GT", 12 | "value": "5", 13 | "inverted": false, 14 | "caseInsensitive": true 15 | }, 16 | { 17 | "contextName": "dateLastWin", 18 | "operator": "DATE_AFTER", 19 | "value": "2022-05-01T12:00:00.000Z", 20 | "inverted": false, 21 | "caseInsensitive": true 22 | } 23 | ] 24 | }, 25 | { 26 | "id": 1, 27 | "name": "hasEnoughFollowers", 28 | "description": null, 29 | "constraints": [ 30 | { 31 | "contextName": "followers", 32 | "operator": "NUM_GT", 33 | "value": "1000", 34 | "inverted": false, 35 | "caseInsensitive": true 36 | } 37 | ] 38 | }, 39 | { 40 | "id": 2, 41 | "name": "isSingle", 42 | "description": null, 43 | "constraints": [ 44 | { 45 | "contextName": "single", 46 | "operator": "STR_CONTAINS", 47 | "values": [ 48 | "true" 49 | ], 50 | "inverted": false, 51 | "caseInsensitive": true 52 | } 53 | ] 54 | }, 55 | { 56 | "id": 3, 57 | "name": "isCatPerson", 58 | "description": null, 59 | "constraints": [ 60 | { 61 | "contextName": "catOrDog", 62 | "operator": "STR_CONTAINS", 63 | "values": [ 64 | "cat" 65 | ], 66 | "inverted": false, 67 | "caseInsensitive": true 68 | } 69 | ] 70 | } 71 | ], 72 | "features": [ 73 | { 74 | "name": "Test.variants", 75 | "description": null, 76 | "enabled": true, 77 | "strategies": [ 78 | { 79 | "name": "default", 80 | "segments": [ 81 | 0, 82 | 1, 83 | 2, 84 | 3 85 | ] 86 | } 87 | ], 88 | "variants": [ 89 | { 90 | "name": "variant1", 91 | "weight": 50 92 | }, 93 | { 94 | "name": "variant2", 95 | "weight": 50 96 | } 97 | ], 98 | "createdAt": "2019-01-24T10:41:45.236Z" 99 | }, 100 | { 101 | "name": "Test.currentTime", 102 | "description": null, 103 | "enabled": true, 104 | "strategies": [ 105 | { 106 | "name": "default", 107 | "constraints": [ 108 | { 109 | "contextName": "currentTime", 110 | "operator": "DATE_AFTER", 111 | "value": "2022-01-29T13:00:00.000Z" 112 | } 113 | ] 114 | } 115 | ], 116 | "createdAt": "2019-01-24T10:41:45.236Z" 117 | } 118 | ] 119 | } -------------------------------------------------------------------------------- /src/test/resources/unleash-repo-v2-with-impression-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "features": [ 4 | { 5 | "name": "Test.impressionDataPresent", 6 | "description": "It's true, we do have impression data", 7 | "enabled": true, 8 | "strategies": [ 9 | { 10 | "name": "default" 11 | } 12 | ], 13 | "impressionData": true, 14 | "variants": null, 15 | "createdAt": "2019-01-24T10:38:10.370Z" 16 | }, 17 | { 18 | "name": "Test.impressionDataSetToOff", 19 | "description": "Explicitly no impression data", 20 | "enabled": true, 21 | "strategies": [ 22 | { 23 | "name": "default" 24 | } 25 | ], 26 | "impressionData": false, 27 | "variants": null, 28 | "createdAt": "2019-01-24T10:38:10.370Z" 29 | }, 30 | { 31 | "name": "Test.impressionDataMissing", 32 | "description": "No field defined", 33 | "enabled": true, 34 | "strategies": [ 35 | { 36 | "name": "default" 37 | } 38 | ], 39 | "variants": null, 40 | "createdAt": "2019-01-24T10:38:10.370Z" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/test/resources/unleash-repo-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "segments": [ 4 | { 5 | "id": 1, 6 | "name": "some-name", 7 | "description": null, 8 | "constraints": [ 9 | { 10 | "contextName": "some-name", 11 | "operator": "IN", 12 | "value": "name", 13 | "inverted": false, 14 | "caseInsensitive": true 15 | } 16 | ] 17 | } 18 | ], 19 | "features": [ 20 | { 21 | "name": "Test.old", 22 | "description": "No variants here!", 23 | "enabled": true, 24 | "strategies": [ 25 | { 26 | "name": "default" 27 | } 28 | ], 29 | "variants": null, 30 | "createdAt": "2019-01-24T10:38:10.370Z" 31 | }, 32 | { 33 | "name": "Test.variants", 34 | "description": null, 35 | "enabled": true, 36 | "strategies": [ 37 | { 38 | "name": "default", 39 | "segments": [ 40 | 1 41 | ] 42 | } 43 | ], 44 | "variants": [ 45 | { 46 | "name": "variant1", 47 | "weight": 50 48 | }, 49 | { 50 | "name": "variant2", 51 | "weight": 50 52 | } 53 | ], 54 | "createdAt": "2019-01-24T10:41:45.236Z" 55 | }, 56 | { 57 | "name": "featureX", 58 | "enabled": true, 59 | "strategies": [ 60 | { 61 | "name": "default" 62 | } 63 | ] 64 | }, 65 | { 66 | "name": "featureY", 67 | "enabled": false, 68 | "strategies": [ 69 | { 70 | "name": "baz", 71 | "parameters": { 72 | "foo": "bar" 73 | } 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "featureZ", 79 | "enabled": true, 80 | "strategies": [ 81 | { 82 | "name": "default" 83 | }, 84 | { 85 | "name": "hola", 86 | "parameters": { 87 | "name": "val" 88 | }, 89 | "segments": [ 90 | 1 91 | ] 92 | } 93 | ] 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /src/test/resources/unleash-repo-without-feature-field.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /v10_MIGRATION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Migrating to Unleash-Client-Java 10.0.0 2 | 3 | This guide highlights the key changes you need to be aware of when upgrading to v10.0.0 of the Unleash client. 4 | 5 | ## Custom bootstrapping 6 | 7 | The Bootstrapping interface now requires an `Optional` to be returned rather than a `String`. If the bootstrapper fails to load the feature set, return an `Optional` of empty. 8 | 9 | ## `MoreOperations` 10 | 11 | `MoreOperations` no longer lists `count` or `countVariant`, these are considered internal APIs and are no longer publicly exposed. 12 | 13 | `getFeatureToggleDefinition` no longer returns the complete feature flag definition. Instead, it returns a lightweight Java object that contains the name of the flag, the project that it's bound to, and an optional type parameter that describes the feature flag type in Unleash, such as "experiment" or "killswitch". 14 | 15 | ## Strategies 16 | 17 | The strategy interface has changed to only include the two methods `getName` and `isEnabled`. `isEnabled` now requires both a parameter map and an `UnleashContext`. This only affects users who are implementing custom or fallback strategies. 18 | 19 | ## Events 20 | 21 | The following subscriber functions are no longer available: `togglesBackedUp`, `toggleBackupRestored`, and `togglesBootstrapped`. Subscribing to `featuresBackedUp`, `featuresBackupRestored`, and `featuresBootstrapped` respectively serves the same purpose. These subscribers no longer yield events that contain the full feature flag definition, instead, they expose a `getFeatures` method which returns a list of lightweight Java objects containing the feature name, the type of flag, and the project it's bound to. 22 | 23 | The `togglesFetched` listener now returns a `ClientFeaturesResponse` event, which has an identical `getFeatures` method instead of the full feature flag definitions. 24 | 25 | ## Name changes 26 | 27 | - `Variant` has been moved to the `io.getunleash.variant` namespace. 28 | - The public interface `IFeatureRepository` has been renamed to `FeatureRepository`. 29 | - The concrete implementor of `FeatureRepository` has been renamed to `FeatureRepositoryImpl`. 30 | 31 | 32 | ## Removal of deprecated APIs 33 | 34 | The following public deprecated APIs have been removed: 35 | 36 | ### Methods on `DefaultUnleash`: 37 | - `deprecatedGetVariant` - This computes a variant with an old hash seed. This is no longer supported. 38 | - `getFeatureToggleDefinition` - `DefaultUnleash.more().getFeatureToggleDefinition()`. 39 | - `getFeatureToggleNames` - Use `DefaultUnleash.more().getFeatureToggleNames()` instead. 40 | - `count` - No longer publicly accessible. 41 | 42 | ### Methods on `FakeUnleash`: 43 | - `deprecatedGetVariant` - No longer present on the parent interface, removed for consistency. 44 | 45 | ### Methods on `VariantUtil`: 46 | - `selectDeprecatedVariantHashingAlgo` - Removed since it's no longer required for `deprecatedGetVariant`. 47 | 48 | ### Other classes and interfaces: 49 | 50 | `FeatureToggleRepository` - Use `FeatureRepositoryImpl` instead. 51 | 52 | `ToggleRepository` - Use `FeatureRepository` instead. 53 | 54 | `HttpToggleFetcher` - Use `HttpFeatureFetcher` instead. 55 | 56 | `ToggleFetcher` - Use `FeatureFetcher` instead. 57 | 58 | `JsonToggleCollectionDeserializer` - No alternative needed. 59 | 60 | `JsonToggleParser` - No alternative needed. 61 | 62 | `ToggleBackupHandlerFile` - No alternative needed. 63 | --------------------------------------------------------------------------------