├── .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 |
--------------------------------------------------------------------------------