diff --git a/.github/workflows/central-sync-close.yml b/.github/workflows/central-sync-close.yml deleted file mode 100644 index f42f09d26..000000000 --- a/.github/workflows/central-sync-close.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Central Sync Close - -on: - workflow_dispatch: - inputs: - stagedRepositoryId: - description: "Staged repository id" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # Request close promotion repo - - uses: spring-io/nexus-sync-action@main - with: - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - staging-repo-id: ${{ github.event.inputs.stagedRepositoryId }} - close: true diff --git a/.github/workflows/central-sync-create.yml b/.github/workflows/central-sync-create.yml deleted file mode 100644 index 3bcb93c73..000000000 --- a/.github/workflows/central-sync-create.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Central Sync Create - -on: - workflow_dispatch: - inputs: - buildName: - description: "Artifactory build name" - required: true - buildNumber: - description: "Artifactory build number" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # to get spec file in .github - - uses: actions/checkout@v2 - - # Setup jfrog cli - - uses: jfrog/setup-jfrog-cli@v4 - with: - version: 2.52.10 - env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - - # Extract build id from input - - name: Extract Build Id - run: | - echo JFROG_CLI_BUILD_NAME=${{ github.event.inputs.buildName }} >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=${{ github.event.inputs.buildNumber }} >> $GITHUB_ENV - - # Download released files - - name: Download Release Files - run: | - jf rt download \ - --spec .github/release-files-spec.json \ - --spec-vars "buildname=$JFROG_CLI_BUILD_NAME;buildnumber=$JFROG_CLI_BUILD_NUMBER" - - # Create checksums, signatures and create staging repo on central and upload - - uses: spring-io/nexus-sync-action@main - id: nexus - with: - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - create: true - upload: true - generate-checksums: true - - # Print staging repo id - - name: Print Staging Repo Id - run: echo ${{ steps.nexus.outputs.staged-repository-id }} diff --git a/.github/workflows/central-sync-release.yml b/.github/workflows/central-sync-release.yml deleted file mode 100644 index bcb25161e..000000000 --- a/.github/workflows/central-sync-release.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Central Sync Release - -on: - workflow_dispatch: - inputs: - stagedRepositoryId: - description: "Staged repository id" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # Request release promotion repo - - uses: spring-io/nexus-sync-action@main - with: - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - staging-repo-id: ${{ github.event.inputs.stagedRepositoryId }} - release: true diff --git a/.github/workflows/ci-boot.yml b/.github/workflows/ci-boot.yml deleted file mode 100644 index 4008ea580..000000000 --- a/.github/workflows/ci-boot.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: CI Boot - -on: - workflow_dispatch: - schedule: - - cron: '0 0 * * 1' - -jobs: - - # build and test, then upload logs if failure - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - java: [ 17 ] - boot: [ 3.3.9, 3.4.3 ] - fail-fast: false - - steps: - - uses: actions/checkout@v4 - - name: Setup JDK - uses: actions/setup-java@v4 - with: - distribution: adopt - java-version: ${{ matrix.java }} - cache: gradle - - name: Build boot ${{ matrix.boot }} java ${{ matrix.java }} - run: ./gradlew clean build -PspringBootVersion=${{ matrix.boot }} - env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - - name: Tar Build Logs - if: ${{ failure() }} - run: | - mkdir -p build - tar \ - -zc \ - --ignore-failed-read \ - --file build/buildlogs.tar.gz \ - */build/reports \ - */*/build/reports - - name: Upload Build Logs - if: ${{ failure() }} - uses: actions/upload-artifact@v4.4.3 - with: - name: buildlogs-${{ matrix.boot }}-${{ matrix.java }} - path: build/buildlogs.tar.gz diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59cc9d218..8dae12c1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,88 +10,49 @@ on: jobs: - # build and test, then upload logs if failure build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4.2.2 - - name: Setup JDK - uses: actions/setup-java@v4.7.0 + - name: Spring Gradle Build Action + uses: spring-io/spring-gradle-build-action@v2 with: - distribution: adopt - java-version: 17 - cache: gradle + java-version: '17' + distribution: 'temurin' - name: Build with Gradle - run: ./gradlew build - env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - - name: Tar Build Logs - if: ${{ failure() }} - run: | - mkdir -p build - tar \ - -zc \ - --ignore-failed-read \ - --file build/buildlogs.tar.gz \ - */build/reports \ - */*/build/reports - - name: Upload Build Logs - if: ${{ failure() }} - uses: actions/upload-artifact@v4.6.0 - with: - name: buildlogs - path: build/buildlogs.tar.gz + run: ./gradlew -Dmaven.repo.local=$(pwd)/deployment-repository clean build distZip publishToMavenLocal - # if build job succeed, build without tests and publish - publish: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.2.2 - - uses: actions/setup-java@v4.7.0 + - name: Deploy artefacts + if: ${{ github.repository == 'spring-projects/spring-statemachine' && github.ref_name == 'main' }} + uses: spring-io/artifactory-deploy-action@v0.0.2 with: - distribution: adopt - java-version: 17 - cache: gradle - - uses: jfrog/setup-jfrog-cli@v4.5.6 - with: - version: 2.73.2 - env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - - # setup non persistent config for jfrog gradle to use server defined in - # secret which points via virtual repo into spring local repo. - # modify default build name and number as we don't like how those - # gets used from action defaults. - - name: Configure JFrog Cli - env: - JFROG_CLI_SERVER_ID: "repo.spring.io" - run: | - jf gradlec \ - --use-wrapper \ - --uses-plugin \ - --deploy-ivy-desc=false \ - --server-id-resolve=repo.spring.io \ - --server-id-deploy=repo.spring.io \ - --repo-resolve=snapshot \ - --repo-deploy=snapshot - echo JFROG_CLI_BUILD_NAME=spring-statemachine-main >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + build-name: 'spring-statemachine-main' + repository: 'libs-snapshot-local' + folder: 'deployment-repository' - # as we already tested just build and publish - - name: Build and Publish Snapshot - run: | - jf gradle clean build -x test artifactoryPublish + - name: Deploy documentation + if: ${{ github.repository == 'spring-projects/spring-statemachine' && github.ref_name == 'main' }} env: - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - # publish build info so that we can see it in artifactory "builds" - - name: Publish Build Info + DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }} + DOCS_SSH_HOST_KEY: ${{ secrets.DOCS_SSH_HOST_KEY }} + DOCS_HOST: ${{ secrets.DOCS_HOST }} + DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }} + DOCS_PATH: ${{ secrets.DOCS_PATH }} run: | - jf rt build-publish + mkdir "$HOME/.ssh" + echo "$DOCS_SSH_KEY" > "$HOME/.ssh/key" + chmod 600 "$HOME/.ssh/key" + echo "$DOCS_SSH_HOST_KEY" > "$HOME/.ssh/known_hosts" + + PROJECT_VERSION=$(cat gradle.properties | grep "version=" | cut -d '=' -f 2) + ssh -i $HOME/.ssh/key $DOCS_USERNAME@$DOCS_HOST "cd $DOCS_PATH && mkdir -p $PROJECT_VERSION" + + pushd build + scp -i $HOME/.ssh/key -r api $DOCS_USERNAME@$DOCS_HOST:$DOCS_PATH/$PROJECT_VERSION + popd + pushd docs/build/docs + scp -i $HOME/.ssh/key -r asciidoc $DOCS_USERNAME@$DOCS_HOST:$DOCS_PATH/$PROJECT_VERSION/reference + popd diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 000000000..2e660b310 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,47 @@ +name: Deploy Docs + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: "Release version (4.0.x)" + required: true + +jobs: + + publish-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + - name: Spring Gradle Build Action + uses: spring-io/spring-gradle-build-action@v2 + with: + java-version: '17' + distribution: 'temurin' + + - name: Capture project version + run: echo PROJECT_VERSION=${{ github.event.inputs.releaseVersion }} >> $GITHUB_ENV + + - name: Build with Gradle + run: ./gradlew -Pversion=$PROJECT_VERSION distZip + + - name: Deploy documentation + if: ${{ github.repository == 'spring-projects/spring-statemachine' && github.ref_name == 'main' }} + env: + DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }} + DOCS_SSH_HOST_KEY: ${{ secrets.DOCS_SSH_HOST_KEY }} + DOCS_HOST: ${{ secrets.DOCS_HOST }} + DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }} + DOCS_PATH: ${{ secrets.DOCS_PATH }} + run: | + mkdir "$HOME/.ssh" + echo "$DOCS_SSH_KEY" > "$HOME/.ssh/key" + chmod 600 "$HOME/.ssh/key" + echo "$DOCS_SSH_HOST_KEY" > "$HOME/.ssh/known_hosts" + ssh -i $HOME/.ssh/key $DOCS_USERNAME@$DOCS_HOST "cd $DOCS_PATH && mkdir -p $PROJECT_VERSION" + pushd build + scp -i $HOME/.ssh/key -r api $DOCS_USERNAME@$DOCS_HOST:$DOCS_PATH/$PROJECT_VERSION + popd + pushd docs/build/docs + scp -i $HOME/.ssh/key -r asciidoc $DOCS_USERNAME@$DOCS_HOST:$DOCS_PATH/$PROJECT_VERSION/reference + popd diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ccab02e91..973fde2a4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -9,21 +9,17 @@ jobs: # build and test, then upload logs if failure build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - java: [ 17 ] - fail-fast: false + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Setup JDK - uses: actions/setup-java@v1 + - uses: actions/checkout@v4.2.2 + - name: Spring Gradle Build Action + uses: spring-io/spring-gradle-build-action@v2 with: - java-version: ${{ matrix.java }} + java-version: '17' + distribution: 'temurin' - name: Build with Gradle - run: ./gradlew clean build + run: ./gradlew build - name: Tar Build Logs if: ${{ failure() }} run: | @@ -36,7 +32,7 @@ jobs: */*/build/reports - name: Upload Build Logs if: ${{ failure() }} - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.6.2 with: name: buildlogs path: build/buildlogs.tar.gz diff --git a/.github/workflows/release-ga.yml b/.github/workflows/release-ga.yml index 839bebbed..f76662220 100644 --- a/.github/workflows/release-ga.yml +++ b/.github/workflows/release-ga.yml @@ -1,210 +1,31 @@ -# full release workflow which does a staging build, github tagging, -# promotion in artifactory and release into central. -name: Release GA +name: Release to Maven Central on: workflow_dispatch: + inputs: + buildName: + description: "Artifactory build name" + required: true + buildNumber: + description: "Artifactory build number" + required: true -# there's 3 jobs, staging, promote and central. -# promote waits staging and manual approval and -# central waits promote and manual approval. jobs: - # build and release to staging repo. - # stash artifactory build id so that promote and central - # jobs can work on it. - staging: + release: runs-on: ubuntu-latest - outputs: - project-version: ${{ steps.output.outputs.project-version }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 - with: - java-version: 17 - - uses: jfrog/setup-jfrog-cli@v4 - with: - version: 2.52.10 - env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - # prepare env for cli to get a staging build working - - name: Configure JFrog Cli - run: | - jf gradlec \ - --use-wrapper \ - --deploy-ivy-desc=false \ - --server-id-resolve=repo.spring.io \ - --server-id-deploy=repo.spring.io \ - --repo-resolve=libs-release \ - --repo-deploy=libs-staging-local - echo JFROG_CLI_BUILD_NAME=spring-statemachine-main-release >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV - # switch from snapshot to a release version and extract project - # version to get used with tagging - - name: Configure Release Version - run: | - jf gradle releaseVersion - echo PROJECT_VERSION=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') >> $GITHUB_ENV - env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - # build and publish to staging repo. - # we've allready tested with snapshots so no need to test - # with a release build as we are not a release train. - - name: Build and Publish - run: | - jf gradle clean build artifactoryPublish - jf rt build-publish - env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - # we've now done a release build, branch and tag it in github - - name: Tag Release - uses: jvalkeal/build-zoo-handler@v0.0.4 - with: - tag-release-branch: ${{ env.PROJECT_VERSION }} - tag-release-tag: ${{ env.PROJECT_VERSION }} - tag-release-tag-prefix: v - - name: Output - id: output - env: - PROJECT_VERSION: ${{ env.PROJECT_VERSION }} - run: | - echo "::set-output name=project-version::$PROJECT_VERSION" - - # wait manual approval. - # promote build from staging to releases - promote: - runs-on: ubuntu-latest - needs: staging - environment: promote - steps: - # need repo to push release branch and a tag - - uses: actions/checkout@v2 - - uses: jfrog/setup-jfrog-cli@v4 - with: - version: 2.52.10 - env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - # prepare env for cli to promote - - name: Configure JFrog Cli - run: | - jf gradlec \ - --use-wrapper \ - --deploy-ivy-desc=false \ - --server-id-resolve=repo.spring.io \ - --server-id-deploy=repo.spring.io \ - --repo-resolve=libs-release \ - --repo-deploy=libs-staging-local - echo JFROG_CLI_BUILD_NAME=spring-statemachine-main-release >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV - # promoting build from staging repo into release - - name: Promote Build - run: | - jf rt build-promote libs-release-local - # we've promoted so change repo to next dev version - - name: Switch to Next Dev Version - run: | - jf gradle nextVersion - env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - - uses: jvalkeal/build-zoo-handler@v0.0.4 - with: - commit-changes-branch: main - commit-changes-message: Next development version - - # gh release before central - ghrelease: - runs-on: ubuntu-latest - needs: [staging, promote] - steps: - - uses: actions/checkout@v2 - - name: Install Tooling - run: | - curl -sSL https://github.com/cbroglie/mustache/releases/download/v1.2.2/mustache_1.2.2_linux_amd64.tar.gz | sudo tar -C /usr/local/bin/ --no-same-owner -xzv mustache - - name: GitHub Release - env: - PROJECT_VERSION: ${{needs.staging.outputs.project-version}} - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - RELEASE_NOTES_FILE: ${{runner.temp}}/release_notes.md5 - RELEASE_NOTES_DATA: ${{runner.temp}}/release_notes_data.json - RELEASE_NOTES_HEADERS: ${{runner.temp}}/release_notes_headers.json - RELEASE_NOTES_FOOTERS: ${{runner.temp}}/release_notes_footers.json - RELEASE_NOTES_ISSUES: ${{runner.temp}}/release_notes_issues.json - run: | - gh issue list \ - --repo spring-projects/spring-statemachine \ - --milestone $PROJECT_VERSION \ - --label automation/rlnotes-header \ - --state all --json title,body \ - --jq '{headers:map(.),headerslength:(length)}' \ - > $RELEASE_NOTES_HEADERS - gh issue list \ - --repo spring-projects/spring-statemachine \ - --milestone $PROJECT_VERSION \ - --label automation/rlnotes-footer \ - --state all --json title,body \ - --jq '{footers:map(.),footerslength:(length)}' \ - > $RELEASE_NOTES_FOOTERS - gh issue list \ - --repo spring-projects/spring-statemachine \ - --milestone $PROJECT_VERSION \ - --state all --json number,title,labels \ - --jq '{issues:map(select((.labels | length == 0) or (any(.labels[].name; startswith("automation/rlnotes")|not))))}' \ - > $RELEASE_NOTES_ISSUES - jq -s '{issues:(.[0].issues),headers:(.[1].headers),headerslength:(.[1].headerslength),footers:(.[2].footers), footerslength:(.[2].footerslength)}' \ - $RELEASE_NOTES_ISSUES \ - $RELEASE_NOTES_HEADERS \ - $RELEASE_NOTES_FOOTERS \ - > $RELEASE_NOTES_DATA - mustache $RELEASE_NOTES_DATA .github/rlnotes.mustache > $RELEASE_NOTES_FILE - gh release create v$PROJECT_VERSION \ - --draft \ - --title "$PROJECT_VERSION" \ - --notes-file $RELEASE_NOTES_FILE - - # wait manual approval. - # pull released artifacts from repo and do a dance with central sync where we - # create checksum and signature files, create staging repo and upload - # files into it, and then finally close and release that repo. - central: - runs-on: ubuntu-latest - needs: ghrelease - environment: central - steps: - # need repo for spec file - - uses: actions/checkout@v2 - - uses: jfrog/setup-jfrog-cli@v4 - with: - version: 2.52.10 - env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - # prepare env for cli do download released files - - name: Configure JFrog Cli - run: | - echo JFROG_CLI_BUILD_NAME=spring-statemachine-main-release >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV - # download released files for a build. - # spec file defines files we actually need for central - # as release in artifactory contains some test and samples modules - # which we don't want in central. - - name: Download Release Files - run: | - jf rt download \ - --spec .github/release-files-spec.json \ - --spec-vars "buildname=$JFROG_CLI_BUILD_NAME;buildnumber=$JFROG_CLI_BUILD_NUMBER" - # last step, sync to central. - - uses: spring-io/nexus-sync-action@main - with: - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - create: true - upload: true - close: true - release: true - generate-checksums: true + - uses: actions/checkout@v4.2.2 + - name: Set Up JFrog CLI + uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Download Release Artifacts + shell: bash + run: jf rt download --spec .github/release-files-spec.json --spec-vars 'buildname=${{ github.event.inputs.buildName }};buildnumber=${{ github.event.inputs.buildNumber }}' + - name: Sync to Maven Central + uses: spring-io/central-publish-action@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0 + with: + token: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} + token-name: ${{ secrets.CENTRAL_TOKEN_USERNAME }} + timeout: 60m diff --git a/.github/workflows/release-milestone.yml b/.github/workflows/release-milestone.yml deleted file mode 100644 index 0227eb1de..000000000 --- a/.github/workflows/release-milestone.yml +++ /dev/null @@ -1,160 +0,0 @@ -# full release workflow which does a staging build, github tagging, -# promotion in artifactory -name: Release Milestone - -on: - workflow_dispatch: - inputs: - milestone: - description: 'Milestone version like, M1 or RC1, etc' - required: true - -# there's 3 jobs, staging, promote and central. -# promote waits staging and manual approval and -# central waits promote and manual approval. -jobs: - - # build and release to staging repo. - # stash artifactory build id so that promote and central - # jobs can work on it. - staging: - runs-on: ubuntu-latest - outputs: - project-version: ${{ steps.output.outputs.project-version }} - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 - with: - java-version: 17 - - uses: jfrog/setup-jfrog-cli@v4 - with: - version: 2.52.10 - env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - # prepare env for cli to get a staging build working - - name: Configure JFrog Cli - run: | - jf gradlec \ - --use-wrapper \ - --deploy-ivy-desc=false \ - --server-id-resolve=repo.spring.io \ - --server-id-deploy=repo.spring.io \ - --repo-resolve=libs-milestone \ - --repo-deploy=libs-staging-local - echo JFROG_CLI_BUILD_NAME=spring-statemachine-main-milestone >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV - # switch from snapshot to a release version and extract project - # version to get used with tagging - - name: Configure Milestone Version - run: | - jf gradle milestoneVersion -PstatemachineMilestone=${{ github.event.inputs.milestone }} - echo PROJECT_VERSION=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') >> $GITHUB_ENV - env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - # build and publish to staging repo. - # we've allready tested with snapshots so no need to test - # with a release build as we are not a release train. - - name: Build and Publish - run: | - jf gradle clean build artifactoryPublish - jf rt build-publish - env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - # we've now done a release build, branch and tag it in github - - name: Tag Release - uses: jvalkeal/build-zoo-handler@v0.0.4 - with: - tag-release-branch: ${{ env.PROJECT_VERSION }} - tag-release-tag: ${{ env.PROJECT_VERSION }} - tag-release-tag-prefix: v - - name: Output - id: output - env: - PROJECT_VERSION: ${{ env.PROJECT_VERSION }} - run: | - echo "::set-output name=project-version::$PROJECT_VERSION" - - # wait manual approval. - # promote build from staging to milestones - promote: - runs-on: ubuntu-latest - needs: staging - environment: promote - steps: - # need repo to push release branch and a tag - - uses: actions/checkout@v2 - - uses: jfrog/setup-jfrog-cli@v4 - with: - version: 2.52.10 - env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - # prepare env for cli to promote - - name: Configure JFrog Cli - run: | - jf gradlec \ - --use-wrapper \ - --deploy-ivy-desc=false \ - --server-id-resolve=repo.spring.io \ - --server-id-deploy=repo.spring.io \ - --repo-resolve=libs-milestone \ - --repo-deploy=libs-staging-local - echo JFROG_CLI_BUILD_NAME=spring-statemachine-main-milestone >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV - # promoting build from staging repo into release - - name: Promote Build - run: | - jf rt build-promote libs-milestone-local - - ghrelease: - runs-on: ubuntu-latest - needs: [staging, promote] - steps: - - uses: actions/checkout@v2 - - name: Install Tooling - run: | - curl -sSL https://github.com/cbroglie/mustache/releases/download/v1.2.2/mustache_1.2.2_linux_amd64.tar.gz | sudo tar -C /usr/local/bin/ --no-same-owner -xzv mustache - - name: GitHub Release - env: - PROJECT_VERSION: ${{needs.staging.outputs.project-version}} - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - RELEASE_NOTES_FILE: ${{runner.temp}}/release_notes.md5 - RELEASE_NOTES_DATA: ${{runner.temp}}/release_notes_data.json - RELEASE_NOTES_HEADERS: ${{runner.temp}}/release_notes_headers.json - RELEASE_NOTES_FOOTERS: ${{runner.temp}}/release_notes_footers.json - RELEASE_NOTES_ISSUES: ${{runner.temp}}/release_notes_issues.json - run: | - gh issue list \ - --repo spring-projects/spring-statemachine \ - --milestone $PROJECT_VERSION \ - --label automation/rlnotes-header \ - --state all --json title,body \ - --jq '{headers:map(.),headerslength:(length)}' \ - > $RELEASE_NOTES_HEADERS - gh issue list \ - --repo spring-projects/spring-statemachine \ - --milestone $PROJECT_VERSION \ - --label automation/rlnotes-footer \ - --state all --json title,body \ - --jq '{footers:map(.),footerslength:(length)}' \ - > $RELEASE_NOTES_FOOTERS - gh issue list \ - --repo spring-projects/spring-statemachine \ - --milestone $PROJECT_VERSION \ - --state all --json number,title,labels \ - --jq '{issues:map(select((.labels | length == 0) or (any(.labels[].name; startswith("automation/rlnotes")|not))))}' \ - > $RELEASE_NOTES_ISSUES - jq -s '{issues:(.[0].issues),headers:(.[1].headers),headerslength:(.[1].headerslength),footers:(.[2].footers), footerslength:(.[2].footerslength)}' \ - $RELEASE_NOTES_ISSUES \ - $RELEASE_NOTES_HEADERS \ - $RELEASE_NOTES_FOOTERS \ - > $RELEASE_NOTES_DATA - mustache $RELEASE_NOTES_DATA .github/rlnotes.mustache > $RELEASE_NOTES_FILE - gh release create v$PROJECT_VERSION \ - --draft \ - --prerelease \ - --title "$PROJECT_VERSION" \ - --notes-file $RELEASE_NOTES_FILE diff --git a/.github/workflows/stage-release.yml b/.github/workflows/stage-release.yml new file mode 100644 index 000000000..e63dfbe37 --- /dev/null +++ b/.github/workflows/stage-release.yml @@ -0,0 +1,37 @@ +name: Stage Release in Artifactory + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: "Version to stage, e.g. 4.0.1" + required: true + +jobs: + + stage-release: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4.2.2 + + - name: Spring Gradle Build Action + uses: spring-io/spring-gradle-build-action@v2 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build with Gradle + run: ./gradlew -Pversion=${{ github.event.inputs.releaseVersion }} -Dmaven.repo.local=$(pwd)/deployment-repository -x test build publishToMavenLocal + + - name: Deploy to Artifactory + uses: spring-io/artifactory-deploy-action@v0.0.2 + with: + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + build-name: 'spring-statemachine-${{ github.event.inputs.releaseVersion }}' + repository: 'libs-staging-local' + folder: 'deployment-repository' + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6868a2b1..5b9a76191 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,8 +40,8 @@ All commits must include a Signed-off-by trailer at the end of each commit messa For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring). -# Create your branch from master -Create your topic branch to be submitted as a pull request from master. The Spring team will consider your pull request for backporting on a case-by-case basis; you don't need to worry about submitting anything for backporting. +# Create your branch from main +Create your topic branch to be submitted as a pull request from main. The Spring team will consider your pull request for backporting on a case-by-case basis; you don't need to worry about submitting anything for backporting. # Use short branch names Branches used when submitting pull requests should preferably be named according to JIRA issues, e.g. 'SEC-1234'. Otherwise, use succinct, lower-case, dash (-) delimited names, such as 'fix-warnings', 'fix-typo', etc. This is important, because branch names show up in the merge commits that result from accepting pull requests, and should be as expressive and concise as possible. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.adoc b/README.adoc index a1da61e2e..a998643f7 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,10 @@ += Spring Statemachine is no longer maintained + +Please refer to the https://spring.io/blog/2025/04/21/spring-cloud-data-flow-commercial[announcement] for more information. + = Spring Statemachine -image:https://github.com/spring-projects/spring-statemachine/workflows/CI/badge.svg[link="https://github.com/spring-projects/spring-statemachine/actions"] (GitHub default) image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?&search.rootProjectNames=spring-statemachine"] +image:https://github.com/spring-projects/spring-statemachine/workflows/CI/badge.svg[link="https://github.com/spring-projects/spring-statemachine/actions"] (GitHub default) image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?&search.rootProjectNames=spring-statemachine"] The Spring Statemachine project aims to provide a common infrastructure to work with state machine concepts in Spring applications. @@ -41,12 +45,11 @@ invoked from the root of the source tree and serves as a cross-platform, self-contained bootstrap mechanism for the build. === Prerequisites -https://help.github.com/set-up-git-redirect[Git] and the -https://www.oracle.com/technetwork/java/javase/downloads[JDK8 build]. +https://help.github.com/set-up-git-redirect[Git] and https://www.oracle.com/technetwork/java/javase/downloads[JDK17+]. Be sure that your `JAVA_HOME` environment variable points to the -`jdk` folder extracted from the JDK download. While build requires -`JDK8`, created artifacts are `JDK7` compatible. +`jdk` folder extracted from the JDK download. The build requires +`JDK17+`. === Check out sources [indent=0] diff --git a/build.gradle b/build.gradle index b0178e5c3..14d76667d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ plugins { id "base" id 'org.springframework.statemachine.root' + id 'maven-publish' } description = 'Spring Statemachine' repositories { maven { url 'https://repo.spring.io/snapshot' } - maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/release' } mavenCentral() } @@ -16,14 +16,9 @@ allprojects { group = 'org.springframework.statemachine' repositories { - mavenCentral() + maven { url 'https://repo.spring.io/snapshot' } maven { url 'https://repo.spring.io/release' } - if (version.contains('-')) { - maven { url "https://repo.spring.io/milestone" } - } - if (version.endsWith('-SNAPSHOT')) { - maven { url "https://repo.spring.io/snapshot" } - } + mavenCentral() } configurations.all { diff --git a/docs/src/reference/asciidoc/appendix-reactormigration-examples.adoc b/docs/src/reference/asciidoc/appendix-reactormigration-examples.adoc index 1decc9a81..b20820971 100644 --- a/docs/src/reference/asciidoc/appendix-reactormigration-examples.adoc +++ b/docs/src/reference/asciidoc/appendix-reactormigration-examples.adoc @@ -2,4 +2,4 @@ While most of an examples are still same, we've overhauled some of them and created some new: -* Tunrstile Reactive <> +* Turnstile Reactive <> diff --git a/docs/src/reference/asciidoc/appendix-reactormigration.adoc b/docs/src/reference/asciidoc/appendix-reactormigration.adoc index fa0219a8e..8f2d18714 100644 --- a/docs/src/reference/asciidoc/appendix-reactormigration.adoc +++ b/docs/src/reference/asciidoc/appendix-reactormigration.adoc @@ -1,10 +1,10 @@ [appendix] [[appendix-reactormigrationguide]] == Reactor Migration Guide -Main task for a work for `3.x` has been to both internally and externally to move and change +Main task for a work for `3.x` has been to both internally and externally move and change as much as we can from imperative code into a reactive world. This means that some -of a main interfaces has added a new reative methods and most of a internal execution locig -(where applicable) has been moved over to handled by a reactor. Essentially what this means is that thread handling model is considerably different compared to `2.x`. Following chapters +of the main interfaces have added new reactive methods and most of a internal execution logic +(where applicable) has been moved over to be handled by a reactor. Essentially what this means is that thread handling model is considerably different compared to `2.x`. Following chapters go throught all these changes. include::appendix-reactormigration-communicating.adoc[] diff --git a/docs/src/reference/asciidoc/getting-started.adoc b/docs/src/reference/asciidoc/getting-started.adoc index bcf896cc9..7b233d327 100644 --- a/docs/src/reference/asciidoc/getting-started.adoc +++ b/docs/src/reference/asciidoc/getting-started.adoc @@ -17,7 +17,7 @@ dependencies outside of Spring Framework within its core system. Other optional parts (such as <>) have dependencies on Zookeeper, while <> has dependencies -on `spring-shell` and `spring-boot`, which pull other dependencies +on `spring-boot`, which pull other dependencies beyond the framework itself. Also, the optional security and data access features have dependencies to on Spring Security and Spring Data modules. diff --git a/docs/src/reference/asciidoc/recipes.adoc b/docs/src/reference/asciidoc/recipes.adoc index 59f31ca85..49b51eb99 100644 --- a/docs/src/reference/asciidoc/recipes.adoc +++ b/docs/src/reference/asciidoc/recipes.adoc @@ -40,7 +40,7 @@ You can find a sample that shows how to use this recipe at [[statemachine-recipes-tasks]] == Tasks -The tasks recipe is a concept to run DAG (Directed Acrylic Graph) of `Runnable` instances that use +The tasks recipe is a concept to run a DAG (Directed Acyclic Graph) of `Runnable` instances that use a state machine. This recipe has been developed from ideas introduced in <> sample. diff --git a/docs/src/reference/asciidoc/sm-config.adoc b/docs/src/reference/asciidoc/sm-config.adoc index 62a96b531..e441094db 100644 --- a/docs/src/reference/asciidoc/sm-config.adoc +++ b/docs/src/reference/asciidoc/sm-config.adoc @@ -50,7 +50,7 @@ include::samples/DocsConfigurationSampleTests.java[tags=snippetAA] You can also use strings instead of enumerations as states and events by using `StateMachineConfigurerAdapter`, as shown in the next example. Most -of the configuration examples ues enumerations, but, generally speaking, +of the configuration examples use enumerations, but, generally speaking, you can interchange strings and enumerations. ==== diff --git a/docs/src/reference/asciidoc/sm-examples-cdplayer.adoc b/docs/src/reference/asciidoc/sm-examples-cdplayer.adoc index 3d8f6be4c..7a0132c43 100644 --- a/docs/src/reference/asciidoc/sm-examples-cdplayer.adoc +++ b/docs/src/reference/asciidoc/sm-examples-cdplayer.adoc @@ -231,15 +231,15 @@ The following example shows how this state machine actually works. ==== [source,text] ---- -sm>sm start +sm>start Entry state IDLE Entry state CLOSED State machine started -sm>cd lcd +sm>lcd No CD -sm>cd library +sm>list 0: Greatest Hits 0: Bohemian Rhapsody 05:56 1: Another One Bites the Dust 03:36 @@ -247,14 +247,14 @@ sm>cd library 0: A Kind of Magic 04:22 1: Under Pressure 04:08 -sm>cd eject +sm>eject Exit state CLOSED Entry state OPEN -sm>cd load 0 +sm>load 0 Loading cd Greatest Hits -sm>cd play +sm>play Exit state OPEN Entry state CLOSED Exit state CLOSED @@ -262,21 +262,21 @@ Exit state IDLE Entry state BUSY Entry state PLAYING -sm>cd lcd +sm>lcd Greatest Hits Bohemian Rhapsody 00:03 -sm>cd forward +sm>forward -sm>cd lcd +sm>lcd Greatest Hits Another One Bites the Dust 00:04 -sm>cd stop +sm>stop Exit state PLAYING Exit state BUSY Entry state IDLE Entry state CLOSED -sm>cd lcd +sm>lcd Greatest Hits ---- ==== diff --git a/docs/src/reference/asciidoc/sm-examples-persist.adoc b/docs/src/reference/asciidoc/sm-examples-persist.adoc index 03b822c34..7b06686e6 100644 --- a/docs/src/reference/asciidoc/sm-examples-persist.adoc +++ b/docs/src/reference/asciidoc/sm-examples-persist.adoc @@ -44,27 +44,27 @@ The following example shows the state machine's output: ==== [source,text] ---- -sm>persist db +sm>list Order [id=1, state=PLACED] Order [id=2, state=PROCESSING] Order [id=3, state=SENT] Order [id=4, state=DELIVERED] -sm>persist process 1 +sm>process 1 Exit state PLACED Entry state PROCESSING -sm>persist db +sm>list Order [id=2, state=PROCESSING] Order [id=3, state=SENT] Order [id=4, state=DELIVERED] Order [id=1, state=PROCESSING] -sm>persist deliver 3 +sm>deliver 3 Exit state SENT Entry state DELIVERED -sm>persist db +sm>list Order [id=2, state=PROCESSING] Order [id=4, state=DELIVERED] Order [id=1, state=PROCESSING] diff --git a/docs/src/reference/asciidoc/sm-examples-showcase.adoc b/docs/src/reference/asciidoc/sm-examples-showcase.adoc index 146d4ca95..8d11b8b7a 100644 --- a/docs/src/reference/asciidoc/sm-examples-showcase.adoc +++ b/docs/src/reference/asciidoc/sm-examples-showcase.adoc @@ -83,14 +83,14 @@ various events are sent to it: ==== [source,text] ---- -sm>sm start +sm>start Init foo to 0 Entry state S0 Entry state S1 Entry state S11 State machine started -sm>sm event A +sm>event A Event A send sm>sm event C @@ -101,12 +101,12 @@ Entry state S21 Entry state S211 Event C send -sm>sm event H +sm>event H Switch foo to 1 Internal transition source=S0 Event H send -sm>sm event C +sm>event C Exit state S211 Exit state S21 Exit state S2 @@ -114,7 +114,7 @@ Entry state S1 Entry state S11 Event C send -sm>sm event A +sm>event A Exit state S11 Exit state S1 Entry state S1 @@ -144,27 +144,27 @@ handling works: ==== [source,text] ---- -sm>sm variables +sm>variables No variables -sm>sm start +sm>start Init foo to 0 Entry state S0 Entry state S1 Entry state S11 State machine started -sm>sm variables +sm>variables foo=0 -sm>sm event H +sm>event H Internal transition source=S1 Event H send -sm>sm variables +sm>variables foo=0 -sm>sm event C +sm>event C Exit state S11 Exit state S1 Entry state S2 @@ -172,23 +172,23 @@ Entry state S21 Entry state S211 Event C send -sm>sm variables +sm>variables foo=0 -sm>sm event H +sm>event H Switch foo to 1 Internal transition source=S0 Event H send -sm>sm variables +sm>variables foo=1 -sm>sm event H +sm>event H Switch foo to 0 Internal transition source=S2 Event H send -sm>sm variables +sm>variables foo=0 ---- ==== diff --git a/docs/src/reference/asciidoc/sm-examples-tasks.adoc b/docs/src/reference/asciidoc/sm-examples-tasks.adoc index 37476505b..0e1e070b9 100644 --- a/docs/src/reference/asciidoc/sm-examples-tasks.adoc +++ b/docs/src/reference/asciidoc/sm-examples-tasks.adoc @@ -102,11 +102,11 @@ The following example shows how this state machine actually works: ==== [source,text] ---- -sm>sm start +sm>start State machine started Entry state READY -sm>tasks run +sm>run Exit state READY Entry state TASKS run task on T2 diff --git a/docs/src/reference/asciidoc/sm-examples-turnstile.adoc b/docs/src/reference/asciidoc/sm-examples-turnstile.adoc index 134b3f7ea..b94a2ef0d 100644 --- a/docs/src/reference/asciidoc/sm-examples-turnstile.adoc +++ b/docs/src/reference/asciidoc/sm-examples-turnstile.adoc @@ -3,7 +3,7 @@ Turnstile is a simple device that gives you access if payment is made. It is a concept that is simple to model using a state machine. In its -simplest, form there are only two states: `LOCKED` and `UNLOCKED`. Two +simplest form, there are only two states: `LOCKED` and `UNLOCKED`. Two events, `COIN` and `PUSH` can happen, depending on whether someone makes a payment or tries to go through the turnstile. The following image shows the state machine: @@ -16,7 +16,7 @@ The following listing shows the enumeration that defines the possible states: .States [source,java,indent=0] ---- -include::samples/demo/turnstile/Application.java[tags=snippetB] +include::samples/demo/turnstile/States.java[tags=snippetB] ---- ==== @@ -26,7 +26,7 @@ The following listing shows the enumeration that defines the events: .Events [source,java,indent=0] ---- -include::samples/demo/turnstile/Application.java[tags=snippetC] +include::samples/demo/turnstile/Events.java[tags=snippetC] ---- ==== @@ -36,7 +36,7 @@ The following listing shows the code that configures the state machine: .Configuration [source,java,indent=0] ---- -include::samples/demo/turnstile/Application.java[tags=snippetA] +include::samples/demo/turnstile/StateMachineConfiguration.java[tags=snippetA] ---- ==== @@ -49,7 +49,7 @@ and shows the command's output: ---- $ java -jar spring-statemachine-samples-turnstile-{revnumber}.jar -sm>sm print +sm>print +----------------------------------------------------------------+ | SM | +----------------------------------------------------------------+ @@ -70,15 +70,15 @@ sm>sm print | | +----------------------------------------------------------------+ -sm>sm start +sm>start State changed to LOCKED State machine started -sm>sm event COIN +sm>event COIN State changed to UNLOCKED Event COIN send -sm>sm event PUSH +sm>event PUSH State changed to LOCKED Event PUSH send ---- diff --git a/docs/src/reference/asciidoc/sm-examples-turnstilereactive.adoc b/docs/src/reference/asciidoc/sm-examples-turnstilereactive.adoc index 8d8f2fcf6..98191256f 100644 --- a/docs/src/reference/asciidoc/sm-examples-turnstilereactive.adoc +++ b/docs/src/reference/asciidoc/sm-examples-turnstilereactive.adoc @@ -1,7 +1,7 @@ [[statemachine-examples-turnstilereactive]] == Turnstile Reactive -Turnstile reactive is an enhacement to <> sample using +Turnstile reactive is an enhancement to <> sample using same _StateMachine_ concept and adding a reactive web layer communicating reactively with a _StateMachine_ reactive interfaces. diff --git a/docs/src/reference/asciidoc/sm-examples-washer.adoc b/docs/src/reference/asciidoc/sm-examples-washer.adoc index 495fb4cde..d0f83f4ea 100644 --- a/docs/src/reference/asciidoc/sm-examples-washer.adoc +++ b/docs/src/reference/asciidoc/sm-examples-washer.adoc @@ -57,28 +57,28 @@ The following example shows how this state machine actually works: ==== [source,text] ---- -sm>sm start +sm>start Entry state RUNNING Entry state WASHING State machine started -sm>sm event RINSE +sm>event RINSE Exit state WASHING Entry state RINSING Event RINSE send -sm>sm event DRY +sm>event DRY Exit state RINSING Entry state DRYING Event DRY send -sm>sm event CUTPOWER +sm>event CUTPOWER Exit state DRYING Exit state RUNNING Entry state POWEROFF Event CUTPOWER send -sm>sm event RESTOREPOWER +sm>event RESTOREPOWER Exit state POWEROFF Entry state RUNNING Entry state WASHING diff --git a/docs/src/reference/asciidoc/sm-examples-zookeeper.adoc b/docs/src/reference/asciidoc/sm-examples-zookeeper.adoc index 0326ed40d..591cbaa68 100644 --- a/docs/src/reference/asciidoc/sm-examples-zookeeper.adoc +++ b/docs/src/reference/asciidoc/sm-examples-zookeeper.adoc @@ -48,16 +48,16 @@ The following example shows what happens: .Shell1 [source,text] ---- -sm>sm start +sm>start Entry state LOCKED State machine started -sm>sm event COIN +sm>event COIN Exit state LOCKED Entry state UNLOCKED Event COIN send -sm>sm state +sm>state UNLOCKED ---- ==== @@ -73,10 +73,10 @@ The following example shows the state machine and its output: .Shell2 [source,text] ---- -sm>sm start +sm>start State machine started -sm>sm state +sm>state UNLOCKED ---- ==== @@ -89,7 +89,7 @@ The following example shows the state machine command and its output: .Shell2 [source,text] ---- -sm>sm event PUSH +sm>event PUSH Exit state UNLOCKED Entry state LOCKED Event PUSH send diff --git a/docs/src/reference/asciidoc/sm-examples.adoc b/docs/src/reference/asciidoc/sm-examples.adoc index 7a6d264fe..0f63ee741 100644 --- a/docs/src/reference/asciidoc/sm-examples.adoc +++ b/docs/src/reference/asciidoc/sm-examples.adoc @@ -58,8 +58,7 @@ The following listing shows how to build the samples: ==== Every sample is located in its own directory under -`spring-statemachine-samples`. The samples are based on Spring Boot and -Spring Shell, and you can find the usual Boot fat jars under every sample +`spring-statemachine-samples`. The samples are based on Spring Boot and you can find the usual Boot fat jars under every sample project's `build/libs` directory. NOTE: The filenames for the jars to which we refer in this section are populated during a diff --git a/docs/src/reference/asciidoc/sm-repository.adoc b/docs/src/reference/asciidoc/sm-repository.adoc index 7000d642f..c524a6209 100644 --- a/docs/src/reference/asciidoc/sm-repository.adoc +++ b/docs/src/reference/asciidoc/sm-repository.adoc @@ -63,7 +63,7 @@ model regarding used table names. Previously, generated table names always had a prefix of `JPA_REPOSITORY_`, derived from entity class names. As this caused breaking issues with databases imposing restrictions on database object lengths, all entity classes have -spesific definitions to force table names. For example, +specific definitions to force table names. For example, `JPA_REPOSITORY_STATE` is now 'STATE' -- and so on with other ntity classes. diff --git a/docs/src/reference/asciidoc/whatsnew.adoc b/docs/src/reference/asciidoc/whatsnew.adoc index e7280e1af..b03125b96 100644 --- a/docs/src/reference/asciidoc/whatsnew.adoc +++ b/docs/src/reference/asciidoc/whatsnew.adoc @@ -73,8 +73,16 @@ in a future releases. [IMPORTANT] ==== Please read an appendix <> carefully as it will steer you -through a process of migrating into `3.x` for cases we're not handling internallyl. +through a process of migrating into `3.x` for cases we're not handling internally. ==== At this point most of a documentation has been changed to showcase reactive interfaces while we still keep some notes around to users still using old blocking methods. + +== In 4.0 + +Spring Statemachine 4.0.x requires Java 17+ and focuses on the support of Spring Boot 3.5.x. + +This release includes several dependencies and documentation updates, as well as some bug fixes. + +The samples have been updated, and the dependency to Spring Shell has been removed from the project. \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 4b1bb41f4..d837e459b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ -version=4.0.1-SNAPSHOT -springBootVersion=3.5.0-SNAPSHOT -springShellVersion=3.4.1-SNAPSHOT +version=4.0.2 +springBootVersion=3.5.15 jakartaPersistenceVersion=3.1.0 +mongoDriverSyncVersion=5.5.2 kryoVersion=4.0.3 eclipseEmfXmiVersion=2.11.1 eclipseUml2CommonVersion=2.0.0-v20140602-0749 @@ -10,11 +10,10 @@ eclipseEmfCommonVersion=2.11.0 eclipseUml2TypesVersion=2.0.0-v20140602-0749 eclipseEmfEcoreVersion=2.11.2 eclipseUml2UmlVersion=5.0.0-v20140602-0749 -xcuratorVersion=2.11.1 -curatorVersion=5.5.0 +curatorVersion=5.9.0 springAsciidoctorBackends=0.0.5 awaitilityVersion=3.1.6 -reactorBlockHoundVersion=1.0.10.RELEASE +reactorBlockHoundVersion=1.0.17.RELEASE findbugsVersion=3.0.2 gradleEnterpriseVersion=3.16.2 springGeConventionsVersion=0.0.15 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9bf7bd339..7a04a2dcf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/publish-maven.gradle b/publish-maven.gradle deleted file mode 100644 index 902e2bf7b..000000000 --- a/publish-maven.gradle +++ /dev/null @@ -1,60 +0,0 @@ -apply plugin: 'maven' - -ext.optionalDeps = [] -ext.providedDeps = [] - -ext.optional = { optionalDeps << it } -ext.provided = { providedDeps << it } - -install { - repositories.mavenInstaller { - customizePom(pom, project) - } -} - -def customizePom(pom, gradleProject) { - pom.whenConfigured { generatedPom -> - // respect 'optional' and 'provided' dependencies - gradleProject.optionalDeps.each { dep -> - generatedPom.dependencies.findAll { it.artifactId == dep.name }*.optional = true - } - gradleProject.providedDeps.each { dep -> - generatedPom.dependencies.findAll { it.artifactId == dep.name }*.scope = 'provided' - } - - // eliminate test-scoped dependencies (no need in maven central poms) - generatedPom.dependencies.removeAll { dep -> - dep.scope == 'test' - } - - // add all items necessary for maven central publication - generatedPom.project { - name = gradleProject.description - description = gradleProject.description - url = 'https://github.com/spring-projects/spring-statemachine' - organization { - name = 'SpringSource' - url = 'https://spring.io/spring-statemachine' - } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'https://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - scm { - url = 'https://github.com/spring-projects/spring-statemachine' - connection = 'scm:git:git://github.com/spring-projects/spring-statemachine' - developerConnection = 'scm:git:git://github.com/spring-projects/spring-statemachine' - } - developers { - developer { - id = 'jvalkeal' - name = 'Janne Valkealahti' - email = 'janne.valkealahti@gmail.com' - } - } - } - } -} diff --git a/settings.gradle b/settings.gradle index e1bba595f..9d1150ee5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,14 +1,9 @@ pluginManagement { repositories { + maven { url 'https://repo.spring.io/snapshot' } + maven { url 'https://repo.spring.io/release' } mavenCentral() gradlePluginPortal() - maven { url 'https://repo.spring.io/release' } - if (version.contains('-')) { - maven { url 'https://repo.spring.io/milestone' } - } - if (version.endsWith('-SNAPSHOT')) { - maven { url 'https://repo.spring.io/snapshot' } - } } plugins { id 'org.springframework.boot' version "$springBootVersion" diff --git a/spring-statemachine-autoconfigure/spring-statemachine-autoconfigure.gradle b/spring-statemachine-autoconfigure/spring-statemachine-autoconfigure.gradle index a0410b5a5..399469be4 100644 --- a/spring-statemachine-autoconfigure/spring-statemachine-autoconfigure.gradle +++ b/spring-statemachine-autoconfigure/spring-statemachine-autoconfigure.gradle @@ -16,8 +16,13 @@ dependencies { optional 'io.micrometer:micrometer-core' optional 'org.springframework.boot:spring-boot-starter-data-jpa' optional 'org.springframework.boot:spring-boot-starter-data-redis' - optional 'org.springframework.boot:spring-boot-starter-data-mongodb' + optional('org.springframework.boot:spring-boot-starter-data-mongodb') { + exclude group: 'org.mongodb', module: 'mongodb-driver-sync' + } testImplementation 'org.springframework.boot:spring-boot-test' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.assertj:assertj-core' testRuntimeOnly 'com.h2database:h2' + testRuntimeOnly 'org.mongodb:mongodb-driver-sync' } diff --git a/spring-statemachine-build-tests/spring-statemachine-build-tests.gradle b/spring-statemachine-build-tests/spring-statemachine-build-tests.gradle index 1994d5c23..99440020a 100644 --- a/spring-statemachine-build-tests/spring-statemachine-build-tests.gradle +++ b/spring-statemachine-build-tests/spring-statemachine-build-tests.gradle @@ -14,11 +14,14 @@ dependencies { testImplementation(testFixtures(project(":spring-statemachine-core"))) testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.apache.commons:commons-pool2' - testRuntimeOnly 'org.springframework.boot:spring-boot-starter-data-mongodb' + testRuntimeOnly('org.springframework.boot:spring-boot-starter-data-mongodb') { + exclude group: 'org.mongodb', module: 'mongodb-driver-sync' + } testRuntimeOnly 'org.springframework.boot:spring-boot-starter-data-redis' testRuntimeOnly 'redis.clients:jedis' - testImplementation 'org.junit.jupiter:junit-jupiter-api' + testRuntimeOnly 'org.mongodb:mongodb-driver-sync' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' testImplementation 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter' diff --git a/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/RedisPersistTests.java b/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/RedisPersistTests.java index fca2b38c9..3fb973378 100644 --- a/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/RedisPersistTests.java +++ b/spring-statemachine-build-tests/src/test/java/org/springframework/statemachine/buildtests/RedisPersistTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,7 +110,12 @@ public RedisConnectionFactory redisConnectionFactory() { @Bean public StateMachinePersist stateMachinePersist(RedisConnectionFactory connectionFactory) { RedisStateMachineContextRepository repository = - new RedisStateMachineContextRepository(connectionFactory); + new RedisStateMachineContextRepository(connectionFactory, + RedisStateMachineContextRepository.DEFAULT_KEY_NAMESPACE, + kryo -> { + kryo.register(TestStates.class); + kryo.register(TestEvents.class); + }); return new RepositoryStateMachinePersist(repository); } diff --git a/spring-statemachine-cluster/spring-statemachine-cluster.gradle b/spring-statemachine-cluster/spring-statemachine-cluster.gradle index 8e50c4ac4..e3ea418cb 100644 --- a/spring-statemachine-cluster/spring-statemachine-cluster.gradle +++ b/spring-statemachine-cluster/spring-statemachine-cluster.gradle @@ -10,6 +10,6 @@ dependencies { api 'org.springframework.integration:spring-integration-zookeeper' testImplementation 'org.apache.curator:curator-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-core/spring-statemachine-core.gradle b/spring-statemachine-core/spring-statemachine-core.gradle index c8739ca14..ebe499190 100644 --- a/spring-statemachine-core/spring-statemachine-core.gradle +++ b/spring-statemachine-core/spring-statemachine-core.gradle @@ -14,7 +14,10 @@ dependencies { compileOnly 'com.google.code.findbugs:jsr305' testImplementation 'org.springframework:spring-web' testImplementation 'org.springframework:spring-webmvc' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-test' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.mockito:mockito-junit-jupiter' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.awaitility:awaitility' testImplementation 'org.springframework.security:spring-security-config' diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java index dec074587..75bd43096 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/TransitionConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -143,8 +143,6 @@ public interface TransitionConfigurer extends * * @param name the name * @return configurer for chaining - * @param name - * @return */ T name(String name); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java index 8037ecc5f..782f6b579 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,7 @@ * then start it again. * * @author Janne Valkealahti + * @author Mahmoud Ben Hassine * * @param the type of state * @param the type of event @@ -143,6 +144,7 @@ public void setExtendedStateVariablesFunction( * @return the state machine context * @deprecated in favour of {@link #buildStateMachineContext(StateMachine, StateMachine, State, Message)} */ + @Deprecated protected StateMachineContext buildStateMachineContext(StateMachine stateMachine, StateMachine rootStateMachine, State state) { return buildStateMachineContext(stateMachine, rootStateMachine, state, null); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/ReactiveStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/ReactiveStateMachineExecutor.java index 26f063bf2..93811fa79 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/ReactiveStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/ReactiveStateMachineExecutor.java @@ -125,7 +125,7 @@ protected Mono doPreStartReactively() { Mono mono = startTriggers(); if (triggerDisposable == null) { - triggerDisposable = triggerFlux.subscribe(); + triggerDisposable = triggerFlux.contextCapture().subscribe(); } if (!initialHandled.getAndSet(true)) { diff --git a/spring-statemachine-data/jpa/spring-statemachine-data-jpa.gradle b/spring-statemachine-data/jpa/spring-statemachine-data-jpa.gradle index 0644f30e5..63435a8d6 100644 --- a/spring-statemachine-data/jpa/spring-statemachine-data-jpa.gradle +++ b/spring-statemachine-data/jpa/spring-statemachine-data-jpa.gradle @@ -14,7 +14,10 @@ dependencies { testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.hsqldb:hsqldb' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-test' + testImplementation 'org.springframework.boot:spring-boot-test-autoconfigure' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.springframework.boot:spring-boot-starter-data-jpa' testRuntimeOnly 'org.springframework.boot:spring-boot-starter-web' } diff --git a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaPersistingStateMachineInterceptor.java b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaPersistingStateMachineInterceptor.java index eba5cb12d..e63c4b328 100644 --- a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaPersistingStateMachineInterceptor.java +++ b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaPersistingStateMachineInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.statemachine.data.jpa; +import java.util.function.Consumer; + +import com.esotericsoftware.kryo.Kryo; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.persist.AbstractPersistingStateMachineInterceptor; import org.springframework.statemachine.persist.StateMachineRuntimePersister; @@ -45,6 +48,21 @@ public JpaPersistingStateMachineInterceptor(JpaStateMachineRepository jpaStateMa this.persist = new JpaRepositoryStateMachinePersist(jpaStateMachineRepository); } + /** + * Instantiates a new jpa persisting state machine interceptor with a Kryo + * customizer for registering application-specific state and event types. + * + * @param jpaStateMachineRepository the jpa state machine repository + * @param kryoCustomizer callback applied to each new Kryo instance after + * the framework defaults; use it to register state/event enums + * @since 4.0.2 + */ + public JpaPersistingStateMachineInterceptor(JpaStateMachineRepository jpaStateMachineRepository, + Consumer kryoCustomizer) { + Assert.notNull(jpaStateMachineRepository, "'jpaStateMachineRepository' must be set"); + this.persist = new JpaRepositoryStateMachinePersist(jpaStateMachineRepository, kryoCustomizer); + } + /** * Instantiates a new jpa persisting state machine interceptor. * diff --git a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java index 72b6f35c6..345905dd1 100644 --- a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java +++ b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.statemachine.data.jpa; +import java.util.function.Consumer; + +import com.esotericsoftware.kryo.Kryo; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.data.RepositoryStateMachinePersist; import org.springframework.statemachine.data.StateMachineRepository; @@ -42,6 +45,21 @@ public JpaRepositoryStateMachinePersist(JpaStateMachineRepository jpaStateMachin this.jpaStateMachineRepository = jpaStateMachineRepository; } + /** + * Instantiates a new jpa repository state machine persist with a Kryo + * customizer for registering application-specific types. + * + * @param jpaStateMachineRepository the jpa state machine repository + * @param kryoCustomizer callback applied to each new Kryo instance after + * the framework defaults; use it to register state/event enums + * @since 4.0.2 + */ + public JpaRepositoryStateMachinePersist(JpaStateMachineRepository jpaStateMachineRepository, + Consumer kryoCustomizer) { + super(kryoCustomizer); + this.jpaStateMachineRepository = jpaStateMachineRepository; + } + /** * Instantiates a new jpa repository state machine persist. * diff --git a/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java b/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java index cfb5fcdf0..b9d08a68d 100644 --- a/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java +++ b/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -457,7 +457,10 @@ public void configure(StateMachineTransitionConfigurer stateMachineRuntimePersister() { - return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository, kryo -> { + kryo.register(PersistTestStates.class); + kryo.register(PersistTestEvents.class); + }); } } diff --git a/spring-statemachine-data/mongodb/spring-statemachine-data-mongodb.gradle b/spring-statemachine-data/mongodb/spring-statemachine-data-mongodb.gradle index da6477054..8227ec296 100644 --- a/spring-statemachine-data/mongodb/spring-statemachine-data-mongodb.gradle +++ b/spring-statemachine-data/mongodb/spring-statemachine-data-mongodb.gradle @@ -12,7 +12,13 @@ dependencies { testImplementation(testFixtures(project(':spring-statemachine-data-common'))) testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation 'io.projectreactor:reactor-test' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.springframework.boot:spring-boot-starter-data-mongodb' + testImplementation 'org.springframework.boot:spring-boot-test' + testImplementation 'org.springframework.boot:spring-boot-test-autoconfigure' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly('org.springframework.boot:spring-boot-starter-data-mongodb') { + exclude group: 'org.mongodb', module: 'mongodb-driver-sync' + } + testRuntimeOnly 'org.mongodb:mongodb-driver-sync' testRuntimeOnly 'org.springframework.boot:spring-boot-starter-web' } diff --git a/spring-statemachine-data/mongodb/src/main/java/org/springframework/statemachine/data/mongodb/MongoDbPersistingStateMachineInterceptor.java b/spring-statemachine-data/mongodb/src/main/java/org/springframework/statemachine/data/mongodb/MongoDbPersistingStateMachineInterceptor.java index 8c78a519d..8252b927b 100644 --- a/spring-statemachine-data/mongodb/src/main/java/org/springframework/statemachine/data/mongodb/MongoDbPersistingStateMachineInterceptor.java +++ b/spring-statemachine-data/mongodb/src/main/java/org/springframework/statemachine/data/mongodb/MongoDbPersistingStateMachineInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.statemachine.data.mongodb; +import java.util.function.Consumer; + +import com.esotericsoftware.kryo.Kryo; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.persist.AbstractPersistingStateMachineInterceptor; import org.springframework.statemachine.persist.StateMachineRuntimePersister; @@ -45,6 +48,21 @@ public MongoDbPersistingStateMachineInterceptor(MongoDbStateMachineRepository mo this.persist = new MongoDbRepositoryStateMachinePersist(mongodbStateMachineRepository); } + /** + * Instantiates a new mongodb persisting state machine interceptor with a + * Kryo customizer for registering application-specific state and event types. + * + * @param mongodbStateMachineRepository the mongodb state machine repository + * @param kryoCustomizer callback applied to each new Kryo instance after + * the framework defaults; use it to register state/event enums + * @since 4.0.2 + */ + public MongoDbPersistingStateMachineInterceptor(MongoDbStateMachineRepository mongodbStateMachineRepository, + Consumer kryoCustomizer) { + Assert.notNull(mongodbStateMachineRepository, "'mongodbStateMachineRepository' must be set"); + this.persist = new MongoDbRepositoryStateMachinePersist(mongodbStateMachineRepository, kryoCustomizer); + } + /** * Instantiates a new mongodb persisting state machine interceptor. * diff --git a/spring-statemachine-data/mongodb/src/main/java/org/springframework/statemachine/data/mongodb/MongoDbRepositoryStateMachinePersist.java b/spring-statemachine-data/mongodb/src/main/java/org/springframework/statemachine/data/mongodb/MongoDbRepositoryStateMachinePersist.java index 625085d55..534ba70e6 100644 --- a/spring-statemachine-data/mongodb/src/main/java/org/springframework/statemachine/data/mongodb/MongoDbRepositoryStateMachinePersist.java +++ b/spring-statemachine-data/mongodb/src/main/java/org/springframework/statemachine/data/mongodb/MongoDbRepositoryStateMachinePersist.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.statemachine.data.mongodb; +import java.util.function.Consumer; + +import com.esotericsoftware.kryo.Kryo; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.data.RepositoryStateMachinePersist; import org.springframework.statemachine.data.StateMachineRepository; @@ -42,6 +45,21 @@ public MongoDbRepositoryStateMachinePersist(MongoDbStateMachineRepository mongod this.mongodbStateMachineRepository = mongodbStateMachineRepository; } + /** + * Instantiates a new mongodb repository state machine persist with a Kryo + * customizer for registering application-specific types. + * + * @param mongodbStateMachineRepository the mongodb state machine repository + * @param kryoCustomizer callback applied to each new Kryo instance after + * the framework defaults; use it to register state/event enums + * @since 4.0.2 + */ + public MongoDbRepositoryStateMachinePersist(MongoDbStateMachineRepository mongodbStateMachineRepository, + Consumer kryoCustomizer) { + super(kryoCustomizer); + this.mongodbStateMachineRepository = mongodbStateMachineRepository; + } + /** * Instantiates a new mongodb repository state machine persist. * diff --git a/spring-statemachine-data/mongodb/src/test/java/org/springframework/statemachine/data/mongodb/MongoDbRepositoryTests.java b/spring-statemachine-data/mongodb/src/test/java/org/springframework/statemachine/data/mongodb/MongoDbRepositoryTests.java index f5a32142e..a07e6941b 100644 --- a/spring-statemachine-data/mongodb/src/test/java/org/springframework/statemachine/data/mongodb/MongoDbRepositoryTests.java +++ b/spring-statemachine-data/mongodb/src/test/java/org/springframework/statemachine/data/mongodb/MongoDbRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -246,7 +246,10 @@ public void configure(StateMachineTransitionConfigurer stateMachineRuntimePersister() { - return new MongoDbPersistingStateMachineInterceptor<>(mongodbStateMachineRepository); + return new MongoDbPersistingStateMachineInterceptor<>(mongodbStateMachineRepository, kryo -> { + kryo.register(PersistTestStates.class); + kryo.register(PersistTestEvents.class); + }); } } diff --git a/spring-statemachine-data/redis/spring-statemachine-data-redis.gradle b/spring-statemachine-data/redis/spring-statemachine-data-redis.gradle index 81ecb33ae..b3b851a6e 100644 --- a/spring-statemachine-data/redis/spring-statemachine-data-redis.gradle +++ b/spring-statemachine-data/redis/spring-statemachine-data-redis.gradle @@ -12,7 +12,10 @@ dependencies { testImplementation(testFixtures(project(':spring-statemachine-data-common'))) testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation 'io.projectreactor:reactor-test' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-test' + testImplementation 'org.springframework.boot:spring-boot-test-autoconfigure' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.apache.commons:commons-pool2' testRuntimeOnly 'redis.clients:jedis' testRuntimeOnly 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisPersistingStateMachineInterceptor.java b/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisPersistingStateMachineInterceptor.java index 9ec3761a0..92650f173 100644 --- a/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisPersistingStateMachineInterceptor.java +++ b/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisPersistingStateMachineInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.statemachine.data.redis; +import java.util.function.Consumer; + +import com.esotericsoftware.kryo.Kryo; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.persist.AbstractPersistingStateMachineInterceptor; import org.springframework.statemachine.persist.StateMachineRuntimePersister; @@ -45,6 +48,21 @@ public RedisPersistingStateMachineInterceptor(RedisStateMachineRepository redisS this.persist = new RedisRepositoryStateMachinePersist(redisStateMachineRepository); } + /** + * Instantiates a new redis persisting state machine interceptor with a + * Kryo customizer for registering application-specific state and event types. + * + * @param redisStateMachineRepository the redis state machine repository + * @param kryoCustomizer callback applied to each new Kryo instance after + * the framework defaults; use it to register state/event enums + * @since 4.0.2 + */ + public RedisPersistingStateMachineInterceptor(RedisStateMachineRepository redisStateMachineRepository, + Consumer kryoCustomizer) { + Assert.notNull(redisStateMachineRepository, "'redisStateMachineRepository' must be set"); + this.persist = new RedisRepositoryStateMachinePersist(redisStateMachineRepository, kryoCustomizer); + } + /** * Instantiates a new redis persisting state machine interceptor. * diff --git a/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisRepositoryStateMachinePersist.java b/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisRepositoryStateMachinePersist.java index aa5473505..5d586222e 100644 --- a/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisRepositoryStateMachinePersist.java +++ b/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisRepositoryStateMachinePersist.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.statemachine.data.redis; +import java.util.function.Consumer; + +import com.esotericsoftware.kryo.Kryo; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.data.RepositoryStateMachinePersist; import org.springframework.statemachine.data.StateMachineRepository; @@ -42,6 +45,21 @@ public RedisRepositoryStateMachinePersist(RedisStateMachineRepository redisState this.redisStateMachineRepository = redisStateMachineRepository; } + /** + * Instantiates a new redis repository state machine persist with a Kryo + * customizer for registering application-specific types. + * + * @param redisStateMachineRepository the redis state machine repository + * @param kryoCustomizer callback applied to each new Kryo instance after + * the framework defaults; use it to register state/event enums + * @since 4.0.2 + */ + public RedisRepositoryStateMachinePersist(RedisStateMachineRepository redisStateMachineRepository, + Consumer kryoCustomizer) { + super(kryoCustomizer); + this.redisStateMachineRepository = redisStateMachineRepository; + } + /** * Instantiates a new redis repository state machine persist. * diff --git a/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisStateMachineContextRepository.java b/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisStateMachineContextRepository.java index 95e8aeaac..bfc81daf7 100644 --- a/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisStateMachineContextRepository.java +++ b/spring-statemachine-data/redis/src/main/java/org/springframework/statemachine/data/redis/RedisStateMachineContextRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,18 +17,16 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.util.UUID; +import java.util.function.Consumer; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.messaging.MessageHeaders; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.StateMachineContextRepository; -import org.springframework.statemachine.kryo.MessageHeadersSerializer; -import org.springframework.statemachine.kryo.StateMachineContextSerializer; -import org.springframework.statemachine.kryo.UUIDSerializer; +import org.springframework.statemachine.kryo.KryoStateMachineSerialisationDefaults; +import org.springframework.util.Assert; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; @@ -36,6 +34,18 @@ /** * A {@link StateMachineContextRepository} backed by a redis and kryo serialization. + *

+ * Persisted bytes are deserialised through Kryo with the + * {@linkplain KryoStateMachineSerialisationDefaults safe-by-default class + * allowlist} applied. Applications that use custom state or event types + * (typically enums) must register those types via the + * {@link #RedisStateMachineContextRepository(RedisConnectionFactory, String, Consumer)} + * constructor's {@code kryoCustomizer} parameter; otherwise Kryo will reject + * them with {@code IllegalArgumentException} ("Class is not registered"). + *

+ * Keys are written with a fixed namespace prefix (default + * {@value #DEFAULT_KEY_NAMESPACE}) so that user-supplied {@code machineId} + * values cannot collide with unrelated keys in the same Redis logical database. * * @author Janne Valkealahti * @@ -44,38 +54,80 @@ */ public class RedisStateMachineContextRepository implements StateMachineContextRepository> { - private static final ThreadLocal kryoThreadLocal = new ThreadLocal() { + /** + * Default key namespace prefix used when none is supplied. + */ + public static final String DEFAULT_KEY_NAMESPACE = "ssm:context:"; - @SuppressWarnings("rawtypes") - @Override - protected Kryo initialValue() { - Kryo kryo = new Kryo(); - kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer()); - kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer()); - kryo.addDefaultSerializer(UUID.class, new UUIDSerializer()); - return kryo; - } - }; + private final ThreadLocal kryoThreadLocal; private final RedisOperations redisOperations; + private final String keyNamespace; + /** - * Instantiates a new redis state machine context repository. + * Instantiates a new redis state machine context repository using the + * default key namespace and no extra Kryo registrations. * * @param redisConnectionFactory the redis connection factory */ public RedisStateMachineContextRepository(RedisConnectionFactory redisConnectionFactory) { - redisOperations = createDefaultTemplate(redisConnectionFactory); + this(redisConnectionFactory, DEFAULT_KEY_NAMESPACE, null); + } + + /** + * Instantiates a new redis state machine context repository with a custom + * key namespace prefix and no extra Kryo registrations. + * + * @param redisConnectionFactory the redis connection factory + * @param keyNamespace the prefix prepended to every Redis key + * @since 4.0.2 + */ + public RedisStateMachineContextRepository(RedisConnectionFactory redisConnectionFactory, String keyNamespace) { + this(redisConnectionFactory, keyNamespace, null); + } + + /** + * Instantiates a new redis state machine context repository. + * + * @param redisConnectionFactory the redis connection factory + * @param keyNamespace the prefix prepended to every Redis key. Must not be {@code null}; + * pass an empty string to disable prefixing (not recommended). + * @param kryoCustomizer optional callback invoked once per Kryo instance after the + * framework's default registrations are applied. Use this to register + * application-specific state and event types (typically enums) so that they + * are accepted by the allowlist. May be {@code null}. + * @since 4.0.2 + */ + public RedisStateMachineContextRepository(RedisConnectionFactory redisConnectionFactory, + String keyNamespace, Consumer kryoCustomizer) { + Assert.notNull(redisConnectionFactory, "Redis connection factory must not be null"); + Assert.notNull(keyNamespace, "Key namespace must not be null"); + this.redisOperations = createDefaultTemplate(redisConnectionFactory); + this.keyNamespace = keyNamespace; + this.kryoThreadLocal = ThreadLocal.withInitial(() -> { + Kryo kryo = new Kryo(); + KryoStateMachineSerialisationDefaults.registerDefaults(kryo); + if (kryoCustomizer != null) { + kryoCustomizer.accept(kryo); + } + return kryo; + }); } @Override public void save(StateMachineContext context, String id) { - redisOperations.opsForValue().set(id, serialize(context)); + redisOperations.opsForValue().set(prefixedKey(id), serialize(context)); } @Override public StateMachineContext getContext(String id) { - return deserialize(redisOperations.opsForValue().get(id)); + return deserialize(redisOperations.opsForValue().get(prefixedKey(id))); + } + + private String prefixedKey(String id) { + Assert.notNull(id, "Machine id must not be null"); + return keyNamespace + id; } private static RedisTemplate createDefaultTemplate(RedisConnectionFactory connectionFactory) { diff --git a/spring-statemachine-data/redis/src/test/java/org/springframework/statemachine/data/redis/RedisRepositoryTests.java b/spring-statemachine-data/redis/src/test/java/org/springframework/statemachine/data/redis/RedisRepositoryTests.java index 4a9e9271b..c5098af3c 100644 --- a/spring-statemachine-data/redis/src/test/java/org/springframework/statemachine/data/redis/RedisRepositoryTests.java +++ b/spring-statemachine-data/redis/src/test/java/org/springframework/statemachine/data/redis/RedisRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -246,7 +246,10 @@ public void configure(StateMachineTransitionConfigurer stateMachineRuntimePersister() { - return new RedisPersistingStateMachineInterceptor<>(redisStateMachineRepository); + return new RedisPersistingStateMachineInterceptor<>(redisStateMachineRepository, kryo -> { + kryo.register(PersistTestStates.class); + kryo.register(PersistTestEvents.class); + }); } } diff --git a/spring-statemachine-data/spring-statemachine-data.gradle b/spring-statemachine-data/spring-statemachine-data.gradle index c46edfadd..66e8d7b68 100644 --- a/spring-statemachine-data/spring-statemachine-data.gradle +++ b/spring-statemachine-data/spring-statemachine-data.gradle @@ -15,7 +15,11 @@ dependencies { testFixturesImplementation (project(':spring-statemachine-test')) testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation 'io.projectreactor:reactor-test' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testFixturesImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-test' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' + testFixturesImplementation 'org.springframework.boot:spring-boot-test' + testFixturesImplementation 'org.junit.jupiter:junit-jupiter-engine' + testFixturesImplementation 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.springframework.boot:spring-boot-starter-web' } diff --git a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java index fc734a116..b0cb821ec 100644 --- a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java +++ b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -27,6 +28,8 @@ import org.springframework.statemachine.support.DefaultStateMachineContext; import org.springframework.util.Assert; +import com.esotericsoftware.kryo.Kryo; + /** * Base implementation of a {@link StateMachinePersist} using Spring Data Repositories. * @@ -42,12 +45,35 @@ public abstract class RepositoryStateMachinePersist serialisationService; /** - * Instantiates a new repository state machine persist. + * Instantiates a new repository state machine persist using the default + * {@link KryoStateMachineSerialisationService} with no extra registrations. */ protected RepositoryStateMachinePersist() { this.serialisationService = new KryoStateMachineSerialisationService(); } + /** + * Instantiates a new repository state machine persist using a + * {@link KryoStateMachineSerialisationService} pre-configured with the + * supplied {@code kryoCustomizer}. + *

+ * Use this constructor to register application-specific state and event + * types (typically enums) with Kryo's registration-required allowlist: + *

{@code
+	 * new JpaRepositoryStateMachinePersist<>(repo, kryo -> {
+	 *     kryo.register(MyStates.class);
+	 *     kryo.register(MyEvents.class);
+	 * })
+	 * }
+ * + * @param kryoCustomizer callback applied to each new Kryo instance; + * may be {@code null} + * @since 4.0.2 + */ + protected RepositoryStateMachinePersist(Consumer kryoCustomizer) { + this.serialisationService = new KryoStateMachineSerialisationService(kryoCustomizer); + } + /** * Instantiates a new repository state machine persist. * diff --git a/spring-statemachine-kryo/spring-statemachine-kryo.gradle b/spring-statemachine-kryo/spring-statemachine-kryo.gradle index 3a871406a..0ef82ec2d 100644 --- a/spring-statemachine-kryo/spring-statemachine-kryo.gradle +++ b/spring-statemachine-kryo/spring-statemachine-kryo.gradle @@ -9,6 +9,6 @@ dependencies { api project(':spring-statemachine-core') api 'com.esotericsoftware:kryo-shaded' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/AbstractKryoStateMachineSerialisationService.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/AbstractKryoStateMachineSerialisationService.java index 4255b8600..36bdd6ebd 100644 --- a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/AbstractKryoStateMachineSerialisationService.java +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/AbstractKryoStateMachineSerialisationService.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,7 @@ public Kryo create() { // doesn't match. for now just use below trick before we try // to go fully on beans and get a bean class loader. kryo.setClassLoader(ClassUtils.getDefaultClassLoader()); + KryoStateMachineSerialisationDefaults.registerDefaults(kryo); configureKryoInstance(kryo); return kryo; } diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationDefaults.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationDefaults.java new file mode 100644 index 000000000..ffffe9946 --- /dev/null +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationDefaults.java @@ -0,0 +1,102 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.kryo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.messaging.MessageHeaders; +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.support.DefaultStateMachineContext; +import org.springframework.statemachine.support.ObservableMap; + +import com.esotericsoftware.kryo.Kryo; + +/** + * Static helper that applies the safe-by-default configuration used by Spring + * Statemachine's Kryo-based persistence: enables + * {@link Kryo#setRegistrationRequired(boolean) registrationRequired=true} and + * registers the concrete framework and JDK types known to be written through + * {@link StateMachineContextSerializer}. + * Applications that use custom state or event types (typically enums) need to + * register those types as well. For Spring-Boot or annotation-config usage this + * is done by subclassing {@link KryoStateMachineSerialisationService} and + * overriding + * {@link AbstractKryoStateMachineSerialisationService#configureKryoInstance(Kryo)}. + * For the {@code spring-statemachine-data-redis} and + * {@code spring-statemachine-zookeeper} backends, a {@code Consumer} + * constructor parameter is provided. + * @since 4.0.2 + * @author Spring Statemachine team + */ +public final class KryoStateMachineSerialisationDefaults { + + private KryoStateMachineSerialisationDefaults() { + } + + /** + * Apply the safe-by-default configuration to the supplied Kryo instance. + * + * @param kryo the Kryo instance to configure + */ + public static void registerDefaults(Kryo kryo) { + // Lock down class resolution: any class that has not been explicitly + // registered below (or by application code afterwards) cannot be + // resolved on read, regardless of what bytes the input stream contains. + kryo.setRegistrationRequired(true); + + // Custom serializers for framework types must be installed before the + // matching register() calls so that the registration picks them up + // rather than falling back to FieldSerializer. + kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer<>()); + kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer()); + kryo.addDefaultSerializer(UUID.class, new UUIDSerializer()); + + // Concrete framework types that travel through the wire. + kryo.register(StateMachineContext.class); + kryo.register(DefaultStateMachineContext.class); + kryo.register(MessageHeaders.class); + kryo.register(UUID.class); + + // Common JDK collection / map types that may legitimately appear in + // event headers, extended-state variables, child collections and + // history-state maps. Application code that uses other concrete map or + // collection types must register them explicitly via the appropriate + // extension hook. + kryo.register(HashMap.class); + kryo.register(LinkedHashMap.class); + kryo.register(TreeMap.class); + kryo.register(ConcurrentHashMap.class); + kryo.register(ArrayList.class); + kryo.register(LinkedList.class); + kryo.register(HashSet.class); + kryo.register(LinkedHashSet.class); + kryo.register(TreeSet.class); + + // DefaultExtendedState wraps its variables in an ObservableMap; that + // concrete wrapper class is therefore the type the serializer + // observes for the variables slot. + kryo.register(ObservableMap.class); + } +} diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationService.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationService.java index 45d71fb15..0e330f747 100644 --- a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationService.java +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationService.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,8 @@ */ package org.springframework.statemachine.kryo; -import java.util.UUID; +import java.util.function.Consumer; -import org.springframework.messaging.MessageHeaders; -import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.service.StateMachineSerialisationService; import com.esotericsoftware.kryo.Kryo; @@ -35,6 +33,41 @@ */ public class KryoStateMachineSerialisationService extends AbstractKryoStateMachineSerialisationService { + private final Consumer kryoCustomizer; + + /** + * Instantiates a new kryo state machine serialisation service with no + * extra class registrations beyond the framework defaults. + */ + public KryoStateMachineSerialisationService() { + this.kryoCustomizer = null; + } + + /** + * Instantiates a new kryo state machine serialisation service with an + * application-supplied Kryo customizer. + *

+ * The customizer is invoked once per Kryo instance after the + * framework's safe-by-default registrations have been applied (via + * {@link KryoStateMachineSerialisationDefaults#registerDefaults(Kryo)}). + * Use it to register application-specific types — typically the {@code S} + * and {@code E} enums used by the state machine — so they are accepted by + * the registration-required allowlist: + *

{@code
+	 * new KryoStateMachineSerialisationService<>(kryo -> {
+	 *     kryo.register(MyStates.class);
+	 *     kryo.register(MyEvents.class);
+	 * })
+	 * }
+ * + * @param kryoCustomizer callback applied to each new Kryo instance; + * may be {@code null} + * @since 4.0.2 + */ + public KryoStateMachineSerialisationService(Consumer kryoCustomizer) { + this.kryoCustomizer = kryoCustomizer; + } + @Override protected void doEncode(Kryo kryo, Object object, Output output) { kryo.writeObject(output, object); @@ -45,10 +78,15 @@ protected T doDecode(Kryo kryo, Input input, Class type) { return kryo.readObject(input, type); } + /** + * Applies the optional {@link #kryoCustomizer} supplied at construction + * time. Subclasses may override this to register additional types on top + * of the framework defaults and the customizer. + */ @Override protected void configureKryoInstance(Kryo kryo) { - kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer()); - kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer()); - kryo.addDefaultSerializer(UUID.class, new UUIDSerializer()); + if (kryoCustomizer != null) { + kryoCustomizer.accept(kryo); + } } } diff --git a/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationDefaultsTests.java b/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationDefaultsTests.java new file mode 100644 index 000000000..7f4f06092 --- /dev/null +++ b/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationDefaultsTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.kryo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.HashMap; + +import org.junit.jupiter.api.Test; +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.support.DefaultExtendedState; +import org.springframework.statemachine.support.DefaultStateMachineContext; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Tests for @{link KryoStateMachineSerialisationDefaults}. + */ +public class KryoStateMachineSerialisationDefaultsTests { + + public static class SomeClass { + public String payload; + } + + public enum MyStates { + LOCKED, UNLOCKED + } + + public enum MyEvents { + COIN, PUSH + } + + @Test + public void testKryoDefaults() { + Kryo myKryo = new Kryo(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Output output = new Output(out); + SomeClass someClass = new SomeClass(); + someClass.payload = "foo"; + myKryo.writeClassAndObject(output, someClass); + output.close(); + + Kryo kryo = new Kryo(); + KryoStateMachineSerialisationDefaults.registerDefaults(kryo); + Input input = new Input(new ByteArrayInputStream(out.toByteArray())); + + assertThatThrownBy(() -> kryo.readClassAndObject(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Class is not registered"); + } + + @Test + public void testKryoDefaultsRoundTrip() { + HashMap headers = new HashMap<>(); + headers.put("foo", "bar"); + StateMachineContext original = + new DefaultStateMachineContext<>("S1", "E1", headers, new DefaultExtendedState()); + + KryoStateMachineSerialisationService service = + new KryoStateMachineSerialisationService<>(); + byte[] bytes; + StateMachineContext roundTrip; + try { + bytes = service.serialiseStateMachineContext(original); + roundTrip = service.deserialiseStateMachineContext(bytes); + } catch (Exception ex) { + throw new AssertionError("Round-trip of framework-default payload must not fail", ex); + } + + assertThat(roundTrip.getState()).isEqualTo("S1"); + assertThat(roundTrip.getEvent()).isEqualTo("E1"); + assertThat(roundTrip.getEventHeaders()).containsEntry("foo", "bar"); + } + + @Test + public void applicationEnumsCanBeAddedViaSubclass() { + KryoStateMachineSerialisationService service = + new KryoStateMachineSerialisationService() { + @Override + protected void configureKryoInstance(Kryo kryo) { + kryo.register(MyStates.class); + kryo.register(MyEvents.class); + } + }; + + StateMachineContext original = new DefaultStateMachineContext<>( + MyStates.LOCKED, MyEvents.COIN, new HashMap<>(), new DefaultExtendedState()); + + byte[] bytes; + StateMachineContext roundTrip; + try { + bytes = service.serialiseStateMachineContext(original); + roundTrip = service.deserialiseStateMachineContext(bytes); + } catch (Exception ex) { + throw new AssertionError("Round-trip with user enums must not fail", ex); + } + + assertThat(roundTrip.getState()).isEqualTo(MyStates.LOCKED); + assertThat(roundTrip.getEvent()).isEqualTo(MyEvents.COIN); + } + + @Test + public void applicationEnumsShouldBeRegistered() { + KryoStateMachineSerialisationService service = + new KryoStateMachineSerialisationService<>(); + + StateMachineContext original = new DefaultStateMachineContext<>( + MyStates.LOCKED, MyEvents.COIN, new HashMap<>(), new DefaultExtendedState()); + + assertThatThrownBy(() -> service.serialiseStateMachineContext(original)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Class is not registered"); + } +} diff --git a/spring-statemachine-platform/build.gradle b/spring-statemachine-platform/build.gradle index a3e1340bd..bb7ff8ece 100644 --- a/spring-statemachine-platform/build.gradle +++ b/spring-statemachine-platform/build.gradle @@ -12,7 +12,6 @@ dependencies { api "log4j:log4j:$log4jVersion" api "jakarta.persistence:jakarta.persistence-api:$jakartaPersistenceVersion" api "com.esotericsoftware:kryo-shaded:$kryoVersion" - api "org.springframework.shell:spring-shell-core:$springShellVersion" api "org.eclipse.uml2:uml:$eclipseUml2UmlVersion" api "org.eclipse.uml2:types:$eclipseUml2TypesVersion" api "org.eclipse.uml2:common:$eclipseUml2CommonVersion" @@ -24,5 +23,6 @@ dependencies { api "org.awaitility:awaitility:$awaitilityVersion" api "io.projectreactor.tools:blockhound:$reactorBlockHoundVersion" api "io.projectreactor.tools:blockhound-junit-platform:$reactorBlockHoundVersion" + api "org.mongodb:mongodb-driver-sync:$mongoDriverSyncVersion" } } \ No newline at end of file diff --git a/spring-statemachine-platform/spring-statemachine-platform.gradle b/spring-statemachine-platform/spring-statemachine-platform.gradle index a14b37b9c..66d1f5909 100644 --- a/spring-statemachine-platform/spring-statemachine-platform.gradle +++ b/spring-statemachine-platform/spring-statemachine-platform.gradle @@ -10,7 +10,6 @@ description = 'Spring Statemachine BOM' dependencies { api platform("org.springframework.boot:spring-boot-dependencies:$springBootVersion") - api platform("org.springframework.shell:spring-shell-dependencies:$springShellVersion") constraints { api "com.google.code.findbugs:jsr305:$findbugsVersion" api "com.esotericsoftware:kryo-shaded:$kryoVersion" @@ -25,5 +24,6 @@ dependencies { api "org.apache.curator:curator-test:$curatorVersion" api "io.projectreactor.tools:blockhound:$reactorBlockHoundVersion" api "io.projectreactor.tools:blockhound-junit-platform:$reactorBlockHoundVersion" + api "org.mongodb:mongodb-driver-sync:$mongoDriverSyncVersion" } } diff --git a/spring-statemachine-recipes/spring-statemachine-recipes.gradle b/spring-statemachine-recipes/spring-statemachine-recipes.gradle index 208e2c566..ebd5638f1 100644 --- a/spring-statemachine-recipes/spring-statemachine-recipes.gradle +++ b/spring-statemachine-recipes/spring-statemachine-recipes.gradle @@ -10,8 +10,8 @@ dependencies { testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.springframework:spring-test' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' testImplementation 'io.projectreactor.tools:blockhound' testImplementation 'org.assertj:assertj-core' } diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index 49c2e8557..bd862de03 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -20,7 +20,9 @@ project('spring-statemachine-samples-turnstilereactive') { api 'org.springframework.boot:spring-boot-starter-webflux' testImplementation(testFixtures(project(":spring-statemachine-core"))) testImplementation 'io.projectreactor:reactor-test' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-test' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } } @@ -228,6 +230,9 @@ project('spring-statemachine-samples-datapersist') { api('org.springframework.boot:spring-boot-starter-data-jpa') api('org.springframework.boot:spring-boot-starter-data-redis') api('org.springframework.boot:spring-boot-starter-data-mongodb') + api('org.springframework.boot:spring-boot-starter-data-mongodb') { + exclude group: 'org.mongodb', module: 'mongodb-driver-sync' + } api('org.springframework.boot:spring-boot-devtools') api('com.h2database:h2') testImplementation(testFixtures(project(":spring-statemachine-core"))) diff --git a/spring-statemachine-samples/cdplayer/spring-statemachine-samples-cdplayer.gradle b/spring-statemachine-samples/cdplayer/spring-statemachine-samples-cdplayer.gradle index 5663848c6..4f45cfe27 100644 --- a/spring-statemachine-samples/cdplayer/spring-statemachine-samples-cdplayer.gradle +++ b/spring-statemachine-samples/cdplayer/spring-statemachine-samples-cdplayer.gradle @@ -8,7 +8,7 @@ dependencies { management platform(project(":spring-statemachine-platform")) implementation project(':spring-statemachine-samples-common') implementation project(':spring-statemachine-core') - implementation 'org.springframework.shell:spring-shell-core' + implementation 'org.springframework.boot:spring-boot-starter' testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Application.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Application.java index 3ea1f51da..b570009d6 100644 --- a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Application.java +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/Application.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; @@ -37,7 +38,7 @@ import reactor.core.publisher.Mono; -@Configuration +@SpringBootApplication(scanBasePackages = "demo") public class Application { @Configuration diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayer.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayer.java index f1d13a388..80913fabc 100644 --- a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayer.java +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayer.java @@ -92,7 +92,7 @@ public void back() { .subscribe(); } - public String getLdcStatus() { + public String getLcdStatus() { return cdStatus + " " + trackStatus; } diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayerCommands.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayerCommands.java index 8c5685e30..c05a6b7ed 100644 --- a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayerCommands.java +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/CdPlayerCommands.java @@ -18,12 +18,15 @@ import java.text.SimpleDateFormat; import java.util.Date; +import demo.AbstractStateMachineCommands; +import demo.BasicCommand; +import demo.Command; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; -@Command -public class CdPlayerCommands { +@Configuration +public class CdPlayerCommands extends AbstractStateMachineCommands { @Autowired private CdPlayer cdPlayer; @@ -31,67 +34,119 @@ public class CdPlayerCommands { @Autowired private Library library; - @Command(command = "cd lcd", description = "Prints CD player lcd info") - public String lcd() { - return cdPlayer.getLdcStatus(); + @Bean + public Command lcd() { + return new BasicCommand("lcd", "Prints CD player lcd info") { + @Override + public String execute(String[] args) { + return cdPlayer.getLcdStatus(); + } + }; } - @Command(command = "cd library", description = "List user CD library") - public String library() { - SimpleDateFormat format = new SimpleDateFormat("mm:ss"); - StringBuilder buf = new StringBuilder(); - int i1 = 0; - for (Cd cd : library.getCollection()) { - buf.append(i1++ + ": " + cd.getName() + "\n"); - int i2 = 0; - for (Track track : cd.getTracks()) { - buf.append(" " + i2++ + ": " + track.getName() + " " + format.format(new Date(track.getLength()*1000)) + "\n"); + @Bean + public Command list() { + return new BasicCommand("list", "List user CD library") { + @Override + public String execute(String[] args) { + SimpleDateFormat format = new SimpleDateFormat("mm:ss"); + StringBuilder buf = new StringBuilder(); + int i1 = 0; + for (Cd cd : library.getCollection()) { + buf.append(i1++ + ": " + cd.getName() + "\n"); + int i2 = 0; + for (Track track : cd.getTracks()) { + buf.append(" " + i2++ + ": " + track.getName() + " " + format.format(new Date(track.getLength()*1000)) + "\n"); + } + } + return buf.toString(); } - } - return buf.toString(); + }; } - @Command(command = "cd load", description = "Load CD into player") - public String load(@Option(longNames = {"", "index"}) int index) { - StringBuilder buf = new StringBuilder(); - try { - Cd cd = library.getCollection().get(index); - cdPlayer.load(cd); - buf.append("Loading cd " + cd); - } catch (Exception e) { - buf.append("Cd with index " + index + " not found, check library"); - } - return buf.toString(); + @Bean + public Command load() { + return new BasicCommand("load [index]", "Load CD [index] into player") { + @Override + public String execute(String[] args) { + StringBuilder buf = new StringBuilder(); + int index = Integer.parseInt(args[0]); + try { + Cd cd = library.getCollection().get(index); + cdPlayer.load(cd); + buf.append("Loading cd " + cd); + } catch (Exception e) { + buf.append("Cd with index " + index + " not found, check library"); + } + return buf.toString(); + } + }; } - @Command(command = "cd play", description = "Press player play button") - public void play() { - cdPlayer.play(); + @Bean + public Command play() { + return new BasicCommand("play", "Press player play button") { + @Override + public String execute(String[] args) { + cdPlayer.play(); + return ""; + } + }; } - @Command(command = "cd stop", description = "Press player stop button") - public void stop() { - cdPlayer.stop(); + @Bean + public Command stop() { + return new BasicCommand("stop", "Press player stop button") { + @Override + public String execute(String[] args) { + cdPlayer.stop(); + return ""; + } + }; } - @Command(command = "cd pause", description = "Press player pause button") - public void pause() { - cdPlayer.pause(); + @Bean + public Command pause() { + return new BasicCommand("pause", "Press player pause button") { + @Override + public String execute(String[] args) { + cdPlayer.pause(); + return ""; + } + }; } - @Command(command = "cd eject", description = "Press player eject button") - public void eject() { - cdPlayer.eject(); + @Bean + public Command eject() { + return new BasicCommand("eject", "Press player eject button") { + @Override + public String execute(String[] args) { + cdPlayer.eject(); + return ""; + } + }; } - @Command(command = "cd forward", description = "Press player forward button") - public void forward() { - cdPlayer.forward(); + @Bean + public Command forward() { + return new BasicCommand("forward", "Press player forward button") { + @Override + public String execute(String[] args) { + cdPlayer.forward(); + return ""; + } + }; } - @Command(command = "cd back", description = "Press player back button") - public void back() { - cdPlayer.back(); + @Bean + public Command back() { + return new BasicCommand("back", "Press player back button") { + @Override + public String execute(String[] args) { + cdPlayer.back(); + return ""; + } + }; } } diff --git a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/StateMachineCommands.java b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/StateMachineCommands.java index 2125ea516..2748781fc 100644 --- a/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/StateMachineCommands.java +++ b/spring-statemachine-samples/cdplayer/src/main/java/demo/cdplayer/StateMachineCommands.java @@ -15,24 +15,32 @@ */ package demo.cdplayer; +import demo.BasicCommand; +import demo.Command; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; import demo.AbstractStateMachineCommands; import demo.cdplayer.Application.Events; import demo.cdplayer.Application.States; import reactor.core.publisher.Mono; -@Command +@Configuration public class StateMachineCommands extends AbstractStateMachineCommands { - @Command(command = "sm event", description = "Sends an event to a state machine") - public String event(@Option(longNames = { "", "event" }, required = true, description = "The event") final Events event) { - getStateMachine() - .sendEvent(Mono.just(MessageBuilder - .withPayload(event).build())) - .subscribe(); - return "Event " + event + " send"; + @Bean + public Command event() { + return new BasicCommand("sm event", "Sends an event to a state machine") { + @Override + public String execute(String[] args) { + Events event = Events.valueOf(args[0]); + getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(event).build())) + .subscribe(); + return "Event " + event + " sent"; + } + }; } } diff --git a/spring-statemachine-samples/cdplayer/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/cdplayer/src/main/resources/META-INF/spring/spring-shell-plugin.xml deleted file mode 100644 index 1112bb354..000000000 --- a/spring-statemachine-samples/cdplayer/src/main/resources/META-INF/spring/spring-shell-plugin.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/spring-statemachine-samples/cdplayer/src/main/resources/application.properties b/spring-statemachine-samples/cdplayer/src/main/resources/application.properties new file mode 100644 index 000000000..709574239 --- /dev/null +++ b/spring-statemachine-samples/cdplayer/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java b/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java deleted file mode 100644 index 15867ca60..000000000 --- a/spring-statemachine-samples/cdplayer/src/test/java/demo/cdplayer/CdPlayerTests.java +++ /dev/null @@ -1,389 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package demo.cdplayer; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.statemachine.TestUtils.doStartAndAssert; -import static org.springframework.statemachine.TestUtils.doStopAndAssert; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.statemachine.ObjectStateMachine; -import org.springframework.statemachine.StateContext; -import org.springframework.statemachine.StateContext.Stage; -import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.StateMachineSystemConstants; -import org.springframework.statemachine.listener.StateMachineListener; -import org.springframework.statemachine.listener.StateMachineListenerAdapter; -import org.springframework.statemachine.state.State; -import org.springframework.statemachine.transition.TransitionKind; - -import demo.CommonConfiguration; -import demo.cdplayer.Application.Events; -import demo.cdplayer.Application.States; - -public class CdPlayerTests { - - private AnnotationConfigApplicationContext context; - - private StateMachine machine; - - private CdPlayer player; - - private Library library; - - private TestListener listener; - - @Test - public void testInitialState() throws InterruptedException { - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(2); - assertThat(machine.getState().getIds()).containsExactly(States.IDLE, States.CLOSED); - assertLcdStatusStartsWith("No CD"); - } - - @Test - public void testEjectTwice() throws Exception { - listener.reset(1, 0, 0); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - assertThat(machine.getState().getIds()).containsExactly(States.IDLE, States.OPEN); - listener.reset(1, 0, 0); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - assertThat(machine.getState().getIds()).containsExactly(States.IDLE, States.CLOSED); - } - - @Test - public void testPlayWithCdLoaded() throws Exception { - listener.reset(1, 0, 0); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(1, 0, 0); - player.load(library.getCollection().get(0)); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(2, 0, 0); - player.play(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(2); - assertThat(machine.getState().getIds()).containsExactly(States.BUSY, States.PLAYING); - assertLcdStatusContains("cd1"); - } - - @Test - public void testPlayWithCdLoadedDeckOpen() throws Exception { - listener.reset(1, 0, 0); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(3, 0, 0); - player.load(library.getCollection().get(0)); - player.play(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(3); - assertThat(machine.getState().getIds()).containsExactly(States.BUSY, States.PLAYING); - assertLcdStatusContains("cd1"); - } - - @Test - public void testPlayWithNoCdLoaded() throws Exception { - listener.reset(0, 0, 0); - player.play(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isZero(); - assertThat(machine.getState().getIds()).containsExactly(States.IDLE, States.CLOSED); - assertLcdStatusStartsWith("No CD"); - } - - @Test - public void testPlayLcdTimeChanges() throws Exception { - listener.reset(1, 0, 0); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(1, 0, 0); - player.load(library.getCollection().get(0)); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(2, 0, 0); - player.play(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(2); - assertThat(machine.getState().getIds()).containsExactly(States.BUSY, States.PLAYING); - assertLcdStatusContains("cd1"); - - listener.reset(0, 0, 0, 0, 1); - assertThat(listener.transitionTimerLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.transitionTimerCount).isEqualTo(1); - assertLcdStatusContains("00:01"); - - listener.reset(0, 0, 0, 0, 1); - assertThat(listener.transitionTimerLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertLcdStatusContains("00:02"); - assertThat(listener.transitionTimerCount).isEqualTo(1); - - listener.reset(0, 0, 0, 0, 2); - assertThat(listener.transitionTimerLatch.await(4, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.transitionTimerCount).isEqualTo(2); - // ok we have some timing problems with - // this test, so for now just check it's - // not previous - assertLcdStatusNotContains("00:02"); - } - - @Test - public void testPlayPause() throws Exception { - listener.reset(1, 0, 0); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(1, 0, 0); - player.load(library.getCollection().get(0)); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(2, 0, 0, 0, 1); - player.play(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(2); - assertThat(listener.transitionTimerLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.transitionTimerCount).isEqualTo(1); - assertThat(machine.getState().getIds()).containsExactly(States.BUSY, States.PLAYING); - assertLcdStatusContains("cd1"); - assertLcdStatusContains("00:01"); - - listener.reset(0, 0, 0, 1, 1); - assertThat(listener.transitionTimerLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertLcdStatusContains("00:02"); - assertThat(listener.transitionTimerCount).isEqualTo(1); - - listener.reset(1, 0, 0, 0); - player.pause(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - assertLcdStatusContains("00:02"); - - listener.reset(1, 0, 0, 1); - player.pause(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - assertThat(listener.transitionLatch.await(2, TimeUnit.SECONDS)).isTrue(); - - listener.reset(0, 0, 0, 2, 2); - assertThat(listener.transitionTimerLatch.await(2100, TimeUnit.MILLISECONDS)).isTrue(); - assertThat(listener.transitionTimerCount).isEqualTo(2); - assertLcdStatusNotContains("00:02"); - } - - @Test - public void testPlayStop() throws Exception { - listener.reset(1, 0, 0); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(1, 0, 0); - player.load(library.getCollection().get(0)); - player.eject(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - - listener.reset(2, 0, 0); - player.play(); - - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(2); - assertThat(machine.getState().getIds()).containsExactly(States.BUSY, States.PLAYING); - - listener.reset(2, 0, 0); - player.stop(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(2); - assertLcdStatusIs("cd1 "); - } - - @Test - public void testPlayDeckOpenNoCd() throws Exception { - listener.reset(2, 0, 0); - player.eject(); - player.play(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(2); - assertThat(machine.getState().getIds()).containsExactly(States.IDLE, States.CLOSED); - } - - private void assertLcdStatusIs(String text) { - assertThat(player.getLdcStatus()).isEqualTo(text); - } - - private void assertLcdStatusStartsWith(String text) { - assertThat(player.getLdcStatus()).startsWith(text); - } - - private void assertLcdStatusContains(String text) { - assertThat(player.getLdcStatus()).contains(text); - } - - private void assertLcdStatusNotContains(String text) { - assertThat(player.getLdcStatus()).doesNotContain(text); - } - - @SuppressWarnings("unchecked") - @BeforeEach - public void setup() throws Exception { - context = new AnnotationConfigApplicationContext(); - context.register(CommonConfiguration.class, Application.class, TestConfig.class); - context.refresh(); - machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); - player = context.getBean(CdPlayer.class); - library = context.getBean(Library.class); - listener = context.getBean(TestListener.class); - doStartAndAssert(machine); - assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS)).isTrue(); - } - - @AfterEach - public void clean() { - doStopAndAssert(machine); - context.close(); - context = null; - machine = null; - player = null; - library = null; - listener = null; - } - - static class TestConfig { - - @Autowired - private StateMachine machine; - - @Bean - public StateMachineListener stateMachineListener() { - TestListener listener = new TestListener(); - machine.addStateListener(listener); - return listener; - } - - @Bean - public Library library() { - // override library to make it easier to test - Track cd1track1 = new Track("cd1track1", 30); - Track cd1track2 = new Track("cd1track2", 30); - Cd cd1 = new Cd("cd1", new Track[]{cd1track1,cd1track2}); - Track cd2track1 = new Track("cd2track1", 30); - Track cd2track2 = new Track("cd2track2", 30); - Cd cd2 = new Cd("cd2", new Track[]{cd2track1,cd2track2}); - return new Library(new Cd[]{cd1,cd2}); - } - - } - - static class TestListener extends StateMachineListenerAdapter { - - volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1); - volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); - volatile CountDownLatch stateEnteredLatch = new CountDownLatch(2); - volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); - volatile CountDownLatch transitionLatch = new CountDownLatch(0); - volatile CountDownLatch transitionTimerLatch = new CountDownLatch(0); - volatile int stateChangedCount = 0; - volatile int transitionCount = 0; - volatile int transitionTimerCount = 0; - List> statesEntered = new ArrayList>(); - List> statesExited = new ArrayList>(); - - @Override - public void stateMachineStarted(StateMachine stateMachine) { - stateMachineStartedLatch.countDown(); - } - - @Override - public void stateChanged(State from, State to) { - stateChangedCount++; - stateChangedLatch.countDown(); - } - - @Override - public void stateEntered(State state) { - statesEntered.add(state); - stateEnteredLatch.countDown(); - } - - @Override - public void stateExited(State state) { - statesExited.add(state); - stateExitedLatch.countDown(); - } - - @Override - public void stateContext(StateContext stateContext) { - if (stateContext.getStage() == Stage.TRANSITION_END) { - if (stateContext.getTransition().getKind() == TransitionKind.INTERNAL - && stateContext.getEvent() == null) { - transitionTimerCount++; - transitionTimerLatch.countDown(); - } else { - transitionCount++; - transitionLatch.countDown(); - } - } - } - - public void reset(int c1, int c2, int c3) { - reset(c1, c2, c3, 0); - } - - public void reset(int c1, int c2, int c3, int c4) { - reset(c1, c2, c3, c4, 0); - } - - public void reset(int c1, int c2, int c3, int c4, int c5) { - stateChangedLatch = new CountDownLatch(c1); - stateEnteredLatch = new CountDownLatch(c2); - stateExitedLatch = new CountDownLatch(c3); - transitionLatch = new CountDownLatch(c4); - transitionTimerLatch = new CountDownLatch(c5); - stateChangedCount = 0; - transitionCount = 0; - transitionTimerCount = 0; - statesEntered.clear(); - statesExited.clear(); - } - } -} diff --git a/spring-statemachine-samples/cdplayer/src/test/resources/log4j2.xml b/spring-statemachine-samples/cdplayer/src/test/resources/log4j2.xml deleted file mode 100644 index 9c440f57c..000000000 --- a/spring-statemachine-samples/cdplayer/src/test/resources/log4j2.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/spring-statemachine-samples/datajpa/spring-statemachine-samples-datajpa.gradle b/spring-statemachine-samples/datajpa/spring-statemachine-samples-datajpa.gradle index 89a912255..7d036bb28 100644 --- a/spring-statemachine-samples/datajpa/spring-statemachine-samples-datajpa.gradle +++ b/spring-statemachine-samples/datajpa/spring-statemachine-samples-datajpa.gradle @@ -21,6 +21,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/datajpamultipersist/spring-statemachine-samples-datajpamultipersist.gradle b/spring-statemachine-samples/datajpamultipersist/spring-statemachine-samples-datajpamultipersist.gradle index 8672d6497..9bc16547b 100644 --- a/spring-statemachine-samples/datajpamultipersist/spring-statemachine-samples-datajpamultipersist.gradle +++ b/spring-statemachine-samples/datajpamultipersist/spring-statemachine-samples-datajpamultipersist.gradle @@ -21,6 +21,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/datapersist/spring-statemachine-samples-datapersist.gradle b/spring-statemachine-samples/datapersist/spring-statemachine-samples-datapersist.gradle index c00069440..c6ec6b455 100644 --- a/spring-statemachine-samples/datapersist/spring-statemachine-samples-datapersist.gradle +++ b/spring-statemachine-samples/datapersist/spring-statemachine-samples-datapersist.gradle @@ -15,7 +15,10 @@ dependencies { implementation('org.springframework.boot:spring-boot-starter-thymeleaf') implementation('org.springframework.boot:spring-boot-starter-data-jpa') implementation('org.springframework.boot:spring-boot-starter-data-redis') - implementation('org.springframework.boot:spring-boot-starter-data-mongodb') + implementation('org.springframework.boot:spring-boot-starter-data-mongodb') { + exclude group: 'org.mongodb', module: 'mongodb-driver-sync' + } + testRuntimeOnly 'org.mongodb:mongodb-driver-sync' implementation('org.springframework.boot:spring-boot-devtools') implementation('com.h2database:h2') testImplementation(testFixtures(project(':spring-statemachine-core'))) @@ -25,6 +28,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/datapersist/src/main/java/demo/datapersist/StateMachineConfig.java b/spring-statemachine-samples/datapersist/src/main/java/demo/datapersist/StateMachineConfig.java index 1bb1fc16b..0a6a48ec9 100644 --- a/spring-statemachine-samples/datapersist/src/main/java/demo/datapersist/StateMachineConfig.java +++ b/spring-statemachine-samples/datapersist/src/main/java/demo/datapersist/StateMachineConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,10 @@ public static class JpaPersisterConfig { @Bean public StateMachineRuntimePersister stateMachineRuntimePersister( JpaStateMachineRepository jpaStateMachineRepository) { - return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository, kryo -> { + kryo.register(States.class); + kryo.register(Events.class); + }); } } //end::snippetA[] @@ -60,8 +63,11 @@ public static class MongoPersisterConfig { @Bean public StateMachineRuntimePersister stateMachineRuntimePersister( - MongoDbStateMachineRepository jpaStateMachineRepository) { - return new MongoDbPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + MongoDbStateMachineRepository mongoDbStateMachineRepository) { + return new MongoDbPersistingStateMachineInterceptor<>(mongoDbStateMachineRepository, kryo -> { + kryo.register(States.class); + kryo.register(Events.class); + }); } } //end::snippetB[] @@ -73,8 +79,11 @@ public static class RedisPersisterConfig { @Bean public StateMachineRuntimePersister stateMachineRuntimePersister( - RedisStateMachineRepository jpaStateMachineRepository) { - return new RedisPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + RedisStateMachineRepository redisStateMachineRepository) { + return new RedisPersistingStateMachineInterceptor<>(redisStateMachineRepository, kryo -> { + kryo.register(States.class); + kryo.register(Events.class); + }); } } //end::snippetC[] diff --git a/spring-statemachine-samples/deploy/spring-statemachine-samples-deploy.gradle b/spring-statemachine-samples/deploy/spring-statemachine-samples-deploy.gradle index e4d3f5ea1..a878654e6 100644 --- a/spring-statemachine-samples/deploy/spring-statemachine-samples-deploy.gradle +++ b/spring-statemachine-samples/deploy/spring-statemachine-samples-deploy.gradle @@ -18,6 +18,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/eventservice/spring-statemachine-samples-eventservice.gradle b/spring-statemachine-samples/eventservice/spring-statemachine-samples-eventservice.gradle index 4d7ed57a7..d35086a20 100644 --- a/spring-statemachine-samples/eventservice/spring-statemachine-samples-eventservice.gradle +++ b/spring-statemachine-samples/eventservice/spring-statemachine-samples-eventservice.gradle @@ -20,6 +20,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/monitoring/spring-statemachine-samples-monitoring.gradle b/spring-statemachine-samples/monitoring/spring-statemachine-samples-monitoring.gradle index 00c00f10a..61d592dd8 100644 --- a/spring-statemachine-samples/monitoring/spring-statemachine-samples-monitoring.gradle +++ b/spring-statemachine-samples/monitoring/spring-statemachine-samples-monitoring.gradle @@ -21,6 +21,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/ordershipping/spring-statemachine-samples-ordershipping.gradle b/spring-statemachine-samples/ordershipping/spring-statemachine-samples-ordershipping.gradle index 1ee21293a..81349ba58 100644 --- a/spring-statemachine-samples/ordershipping/spring-statemachine-samples-ordershipping.gradle +++ b/spring-statemachine-samples/ordershipping/spring-statemachine-samples-ordershipping.gradle @@ -18,6 +18,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/persist/spring-statemachine-samples-persist.gradle b/spring-statemachine-samples/persist/spring-statemachine-samples-persist.gradle index ba723ca08..575b40277 100644 --- a/spring-statemachine-samples/persist/spring-statemachine-samples-persist.gradle +++ b/spring-statemachine-samples/persist/spring-statemachine-samples-persist.gradle @@ -9,7 +9,6 @@ dependencies { implementation project(':spring-statemachine-samples-common') implementation project(':spring-statemachine-recipes-common') implementation 'org.springframework.boot:spring-boot-starter' - implementation 'org.springframework.shell:spring-shell-core' implementation ('org.hsqldb:hsqldb') implementation ('org.springframework:spring-jdbc') testImplementation(testFixtures(project(':spring-statemachine-core'))) @@ -20,6 +19,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java b/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java index fb7aa00cc..551c36be9 100644 --- a/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java @@ -27,7 +27,7 @@ import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler; -@SpringBootApplication +@SpringBootApplication(scanBasePackages = "demo") public class Application { //tag::snippetA[] diff --git a/spring-statemachine-samples/persist/src/main/java/demo/persist/PersistCommands.java b/spring-statemachine-samples/persist/src/main/java/demo/persist/PersistCommands.java index 6f8eeac42..00dcfddad 100644 --- a/spring-statemachine-samples/persist/src/main/java/demo/persist/PersistCommands.java +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/PersistCommands.java @@ -15,34 +15,62 @@ */ package demo.persist; +import demo.BasicCommand; +import demo.Command; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; -@Command +@Configuration public class PersistCommands { @Autowired private Persist persist; - @Command(command = "persist db", description = "List entries from db") - public String listDbEntries() { - return persist.listDbEntries(); + @Bean + public Command list() { + return new BasicCommand("list", "List entries from db") { + @Override + public String execute(String[] args) { + return persist.listDbEntries(); + } + }; } - @Command(command = "persist process", description = "Process order") - public void process(@Option(longNames = {"", "id"}, description = "Order id") int order) { - persist.change(order, "PROCESS"); + @Bean + public Command process() { + return new BasicCommand("process [orderId]", "Process order with [orderId]") { + @Override + public String execute(String[] args) { + int order = Integer.parseInt(args[0]); + persist.change(order, "PROCESS"); + return "Order " + order + " processed"; + } + }; } - @Command(command = "persist send", description = "Send order") - public void send(@Option(longNames = {"", "id"}, description = "Order id") int order) { - persist.change(order, "SEND"); + @Bean + public Command send() { + return new BasicCommand("send [orderId]", "Send order with [orderId]") { + @Override + public String execute(String[] args) { + int order = Integer.parseInt(args[0]); + persist.change(order, "SEND"); + return "Order " + order + " sent"; + } + }; } - @Command(command = "persist deliver", description = "Deliver order") - public void deliver(@Option(longNames = {"", "id"}, description = "Order id") int order) { - persist.change(order, "DELIVER"); + @Bean + public Command deliver() { + return new BasicCommand("deliver [orderId]", "Deliver order with [orderId]") { + @Override + public String execute(String[] args) { + int order = Integer.parseInt(args[0]); + persist.change(order, "DELIVER"); + return "Order " + order + " delivered"; + } + }; } } diff --git a/spring-statemachine-samples/persist/src/main/java/demo/persist/StateMachineCommands.java b/spring-statemachine-samples/persist/src/main/java/demo/persist/StateMachineCommands.java index 2991ba340..4ded9e899 100644 --- a/spring-statemachine-samples/persist/src/main/java/demo/persist/StateMachineCommands.java +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/StateMachineCommands.java @@ -15,22 +15,30 @@ */ package demo.persist; +import demo.BasicCommand; +import demo.Command; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; import demo.AbstractStateMachineCommands; import reactor.core.publisher.Mono; -@Command +@Configuration public class StateMachineCommands extends AbstractStateMachineCommands { - @Command(command = "sm event", description = "Sends an event to a state machine") - public String event(@Option(longNames = { "", "event" }, required = true, description = "The event") final String event) { - getStateMachine() - .sendEvent(Mono.just(MessageBuilder - .withPayload(event).build())) - .subscribe(); - return "Event " + event + " send"; + @Bean + public Command event() { + return new BasicCommand("event", "Sends an event to a state machine") { + @Override + public String execute(String[] args) { + String event = args[0]; + getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(event).build())) + .subscribe(); + return "Event " + event + " sent"; + } + }; } } diff --git a/spring-statemachine-samples/persist/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/persist/src/main/resources/META-INF/spring/spring-shell-plugin.xml deleted file mode 100644 index 1112bb354..000000000 --- a/spring-statemachine-samples/persist/src/main/resources/META-INF/spring/spring-shell-plugin.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/spring-statemachine-samples/persist/src/main/resources/application.properties b/spring-statemachine-samples/persist/src/main/resources/application.properties new file mode 100644 index 000000000..709574239 --- /dev/null +++ b/spring-statemachine-samples/persist/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/spring-statemachine-samples/persist/src/test/java/demo/persist/PersistTests.java b/spring-statemachine-samples/persist/src/test/java/demo/persist/PersistTests.java deleted file mode 100644 index bcb3ccf49..000000000 --- a/spring-statemachine-samples/persist/src/test/java/demo/persist/PersistTests.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package demo.persist; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.statemachine.TestUtils.doStartAndAssert; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.listener.StateMachineListenerAdapter; -import org.springframework.statemachine.state.State; -import org.springframework.statemachine.transition.Transition; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.annotation.DirtiesContext.ClassMode; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import demo.CommonConfiguration; - -@ExtendWith(SpringExtension.class) -@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD) -@SpringBootTest(classes = { CommonConfiguration.class, Application.class, StateMachineCommands.class }) -public class PersistTests { - - @Autowired - private StateMachineCommands commands; - - @Autowired - private StateMachine machine; - - @Autowired - private Persist persist; - - @Test - public void testNotStarted() throws Exception { - assertThat(commands.state()).isEqualTo("No state"); - } - - @Test - public void testInitialState() throws Exception { - TestListener listener = new TestListener(); - machine.addStateListener(listener); - doStartAndAssert(machine); - assertThat(listener.stateChangedLatch.await(3, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateEnteredLatch.await(3, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly("PLACED"); - assertThat(listener.statesEntered).hasSize(1); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo("PLACED"); - assertThat(listener.statesExited).isEmpty(); - } - - @Test - public void testInitialDbList() { - // dataOrder [id=1, state=PLACED]Order [id=2, state=PROCESSING]Order [id=3, state=SENT]Order [id=4, state=DELIVERED] - assertThat(persist.listDbEntries()).contains("PLACED"); - } - - @Test - public void testUpdate1() { - persist.change(1, "PROCESS"); - assertThat(persist.listDbEntries()).contains("id=1, state=PROCESSING"); - } - - @Test - public void testUpdate2() { - persist.change(2, "SEND"); - assertThat(persist.listDbEntries()).contains("id=2, state=SENT"); - } - - private static class TestListener extends StateMachineListenerAdapter { - - volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); - volatile CountDownLatch stateEnteredLatch = new CountDownLatch(1); - volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); - volatile CountDownLatch transitionLatch = new CountDownLatch(0); - volatile List> transitions = new ArrayList>(); - List> statesEntered = new ArrayList>(); - List> statesExited = new ArrayList>(); - - @Override - public void stateChanged(State from, State to) { - stateChangedLatch.countDown(); - } - - @Override - public void stateEntered(State state) { - statesEntered.add(state); - stateEnteredLatch.countDown(); - } - - @Override - public void stateExited(State state) { - statesExited.add(state); - stateExitedLatch.countDown(); - } - - @Override - public void transition(Transition transition) { - transitions.add(transition); - transitionLatch.countDown(); - } - - } - -} diff --git a/spring-statemachine-samples/showcase/spring-statemachine-samples-showcase.gradle b/spring-statemachine-samples/showcase/spring-statemachine-samples-showcase.gradle index 523e6ba6a..25f2bca42 100644 --- a/spring-statemachine-samples/showcase/spring-statemachine-samples-showcase.gradle +++ b/spring-statemachine-samples/showcase/spring-statemachine-samples-showcase.gradle @@ -8,7 +8,7 @@ dependencies { management platform(project(":spring-statemachine-platform")) implementation project(':spring-statemachine-samples-common') implementation project(':spring-statemachine-core') - implementation 'org.springframework.shell:spring-shell-starter' + implementation 'org.springframework.boot:spring-boot-starter' testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation (project(':spring-statemachine-test')) testImplementation 'org.hamcrest:hamcrest-core' @@ -17,6 +17,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java index b1d8fcd43..8f6d4516a 100644 --- a/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java +++ b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java @@ -23,7 +23,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.shell.command.annotation.CommandScan; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; @@ -32,7 +31,6 @@ import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.guard.Guard; -@CommandScan @SpringBootApplication(scanBasePackages = "demo") public class Application { diff --git a/spring-statemachine-samples/showcase/src/main/java/demo/showcase/StateMachineCommands.java b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/StateMachineCommands.java index 9b5761ed3..2f91c46c2 100644 --- a/spring-statemachine-samples/showcase/src/main/java/demo/showcase/StateMachineCommands.java +++ b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/StateMachineCommands.java @@ -15,24 +15,32 @@ */ package demo.showcase; +import demo.BasicCommand; +import demo.Command; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; import demo.AbstractStateMachineCommands; import demo.showcase.Application.Events; import demo.showcase.Application.States; import reactor.core.publisher.Mono; -@Command +@Configuration public class StateMachineCommands extends AbstractStateMachineCommands { - @Command(command = "sm event", description = "Sends an event to a state machine") - public String event(@Option(longNames = { "", "event" }, required = true, description = "The event") final Events event) { - getStateMachine() - .sendEvent(Mono.just(MessageBuilder - .withPayload(event).build())) - .subscribe(); - return "Event " + event + " send"; + @Bean + public Command event() { + return new BasicCommand("event", "Sends an event to a state machine") { + @Override + public String execute(String[] args) { + Events event = Events.valueOf(args[0]); + getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(event).build())) + .subscribe(); + return "Event " + event + " sent"; + } + }; } } diff --git a/spring-statemachine-samples/showcase/src/main/resources/application.properties b/spring-statemachine-samples/showcase/src/main/resources/application.properties index 810a40c47..709574239 100644 --- a/spring-statemachine-samples/showcase/src/main/resources/application.properties +++ b/spring-statemachine-samples/showcase/src/main/resources/application.properties @@ -1 +1 @@ -spring.shell.interactive.enabled=true +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java b/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java deleted file mode 100644 index 71a659eff..000000000 --- a/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package demo.showcase; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.statemachine.TestUtils.doSendEventAndConsumeAll; -import static org.springframework.statemachine.TestUtils.doStartAndAssert; -import static org.springframework.statemachine.TestUtils.doStopAndAssert; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.statemachine.ObjectStateMachine; -import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.StateMachineSystemConstants; -import org.springframework.statemachine.listener.StateMachineListener; -import org.springframework.statemachine.listener.StateMachineListenerAdapter; -import org.springframework.statemachine.state.State; -import org.springframework.statemachine.transition.Transition; - -import demo.CommonConfiguration; -import demo.showcase.Application.Events; -import demo.showcase.Application.States; - -public class ShowcaseTests { - - private AnnotationConfigApplicationContext context; - - private StateMachine machine; - - private TestListener listener; - - @Test - public void testInitialState() throws Exception { - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateEnteredLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S1, States.S11); - assertThat(listener.statesEntered).hasSize(3); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.S0); - assertThat(listener.statesEntered.get(1).getId()).isEqualTo(States.S1); - assertThat(listener.statesEntered.get(2).getId()).isEqualTo(States.S11); - assertThat(listener.statesExited).isEmpty(); - } - - @Test - public void testA() throws Exception { - testInitialState(); - listener.reset(1, 2, 2); - doSendEventAndConsumeAll(machine, Events.A); - // machine.sendEvent(Events.A); - // variable foo is 0, guard denies transition - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isFalse(); - assertThat(listener.stateEnteredLatch.await(1, TimeUnit.SECONDS)).isFalse(); - assertThat(listener.stateExitedLatch.await(1, TimeUnit.SECONDS)).isFalse(); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S1, States.S11); - } - - @Test - public void testB() throws Exception { - testInitialState(); - listener.reset(1, 2, 2); - doSendEventAndConsumeAll(machine, Events.B); - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateEnteredLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateExitedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.statesExited).hasSize(2); - assertThat(listener.statesEntered).hasSize(2); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S1, States.S11); - } - - @Test - public void testCHCA() throws Exception { - testInitialState(); - listener.reset(3, 0, 0); - doSendEventAndConsumeAll(machine, Events.C); - doSendEventAndConsumeAll(machine, Events.H); - doSendEventAndConsumeAll(machine, Events.C); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - - listener.reset(1, 2, 2, 1); - doSendEventAndConsumeAll(machine, Events.A); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); - listener.stateExitedLatch.await(1, TimeUnit.SECONDS); - listener.transitionLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S1, States.S11); - assertThat(listener.statesEntered).hasSize(2); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.S1); - assertThat(listener.statesEntered.get(1).getId()).isEqualTo(States.S11); - assertThat(listener.statesExited).hasSize(2); - assertThat(listener.statesExited.get(0).getId()).isEqualTo(States.S11); - assertThat(listener.statesExited.get(1).getId()).isEqualTo(States.S1); - assertThat(listener.transitionCount).isEqualTo(2); - } - - @Test - public void testC() throws Exception { - testInitialState(); - listener.reset(1, 3, 0); - doSendEventAndConsumeAll(machine, Events.C); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S2, States.S21, States.S211); - assertThat(listener.statesEntered).hasSize(3); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.S2); - assertThat(listener.statesEntered.get(1).getId()).isEqualTo(States.S21); - assertThat(listener.statesEntered.get(2).getId()).isEqualTo(States.S211); - } - - @Test - public void testCC() throws Exception { - testInitialState(); - listener.reset(1, 3, 0); - doSendEventAndConsumeAll(machine, Events.C); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S2, States.S21, States.S211); - listener.reset(1, 2, 0); - doSendEventAndConsumeAll(machine, Events.C); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S1, States.S11); - assertThat(listener.statesEntered).hasSize(2); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.S1); - assertThat(listener.statesEntered.get(1).getId()).isEqualTo(States.S11); - } - - @Test - public void testD() throws Exception { - testInitialState(); - listener.reset(3, 3, 0); - doSendEventAndConsumeAll(machine, Events.D); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S1, States.S11); - assertThat(listener.statesEntered).hasSize(3); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.S0); - assertThat(listener.statesEntered.get(1).getId()).isEqualTo(States.S1); - assertThat(listener.statesEntered.get(2).getId()).isEqualTo(States.S11); - assertThat(listener.statesExited).hasSize(3); - } - - @Test - public void testCD() throws Exception { - testInitialState(); - listener.reset(1, 3, 0); - doSendEventAndConsumeAll(machine, Events.C); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.reset(1, 2, 0); - doSendEventAndConsumeAll(machine, Events.D); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S2, States.S21, States.S211); - assertThat(listener.statesEntered).hasSize(2); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.S21); - assertThat(listener.statesEntered.get(1).getId()).isEqualTo(States.S211); - } - - @Test - public void testI() throws Exception { - testInitialState(); - listener.reset(1, 1, 1); - doSendEventAndConsumeAll(machine, Events.I); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); - listener.stateExitedLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S1, States.S12); - assertThat(listener.statesEntered).hasSize(1); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.S12); - assertThat(listener.statesExited).hasSize(1); - assertThat(listener.statesExited.get(0).getId()).isEqualTo(States.S11); - } - - @Test - public void testII() throws Exception { - testInitialState(); - listener.reset(1, 1, 1); - doSendEventAndConsumeAll(machine, Events.I); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateEnteredLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateExitedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.statesEntered).hasSize(1); - assertThat(listener.statesExited).hasSize(1); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S1, States.S12); - - listener.reset(1, 3, 2); - doSendEventAndConsumeAll(machine, Events.I); - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateEnteredLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateExitedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.statesEntered).hasSize(3); - assertThat(listener.statesExited).hasSize(2); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S2, States.S21, States.S212); - } - - @Test - public void testH() throws Exception { - testInitialState(); - listener.reset(0, 0, 0, 1); - doSendEventAndConsumeAll(machine, Events.H); - listener.transitionLatch.await(1, TimeUnit.SECONDS); - assertThat(listener.transitionCount).isEqualTo(1); - assertThat(listener.transitions.get(0).getSource().getId()).isEqualTo(States.S1); - } - - @Test - public void testCH() throws Exception { - testInitialState(); - doSendEventAndConsumeAll(machine, Events.C); - listener.reset(0, 0, 0, 1); - doSendEventAndConsumeAll(machine, Events.H); - listener.transitionLatch.await(1, TimeUnit.SECONDS); - assertThat(listener.transitionCount).isEqualTo(1); - assertThat(listener.transitions.get(0).getSource().getId()).isEqualTo(States.S0); - } - - @Test - public void testACH() throws Exception { - testInitialState(); - doSendEventAndConsumeAll(machine, Events.A); - doSendEventAndConsumeAll(machine, Events.C); - listener.reset(0, 0, 0, 1); - doSendEventAndConsumeAll(machine, Events.H); - doSendEventAndConsumeAll(machine, Events.A); - listener.transitionLatch.await(1, TimeUnit.SECONDS); - assertThat(listener.transitionCount).isEqualTo(1); - assertThat(listener.transitions.get(0).getSource().getId()).isEqualTo(States.S0); - } - - @Test - public void testE() throws Exception { - testInitialState(); - listener.reset(1, 4, 3, 0); - doSendEventAndConsumeAll(machine, Events.E); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateEnteredLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateExitedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S2, States.S21, States.S211); - assertThat(listener.statesExited).hasSize(3); - assertThat(listener.statesEntered).hasSize(4); - } - - @Test - public void testF() throws Exception { - testInitialState(); - listener.reset(1, 3, 2, 0); - doSendEventAndConsumeAll(machine, Events.F); - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateEnteredLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateExitedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S2, States.S21, States.S211); - assertThat(listener.statesExited).hasSize(2); - assertThat(listener.statesEntered).hasSize(3); - } - - @Test - public void testG() throws Exception { - testInitialState(); - listener.reset(1, 3, 2, 0); - doSendEventAndConsumeAll(machine, Events.G); - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateEnteredLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateExitedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly(States.S0, States.S2, States.S21, States.S211); - assertThat(listener.statesExited).hasSize(2); - assertThat(listener.statesEntered).hasSize(3); - } - - static class Config { - - @Autowired - private StateMachine machine; - - @Bean - public StateMachineListener stateMachineListener() { - TestListener listener = new TestListener(); - machine.addStateListener(listener); - return listener; - } - } - - static class TestListener extends StateMachineListenerAdapter { - - volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); - volatile CountDownLatch stateEnteredLatch = new CountDownLatch(3); - volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); - volatile CountDownLatch transitionLatch = new CountDownLatch(0); - volatile List> transitions = new ArrayList>(); - List> statesEntered = new ArrayList>(); - List> statesExited = new ArrayList>(); - volatile int transitionCount = 0; - - @Override - public void stateChanged(State from, State to) { - stateChangedLatch.countDown(); - } - - @Override - public void stateEntered(State state) { - statesEntered.add(state); - stateEnteredLatch.countDown(); - } - - @Override - public void stateExited(State state) { - statesExited.add(state); - stateExitedLatch.countDown(); - } - - @Override - public void transition(Transition transition) { - transitions.add(transition); - transitionLatch.countDown(); - transitionCount++; - } - - public void reset(int c1, int c2, int c3) { - reset(c1, c2, c3, 0); - } - - public void reset(int c1, int c2, int c3, int c4) { - stateChangedLatch = new CountDownLatch(c1); - stateEnteredLatch = new CountDownLatch(c2); - stateExitedLatch = new CountDownLatch(c3); - transitionLatch = new CountDownLatch(c4); - statesEntered.clear(); - statesExited.clear(); - transitionCount = 0; - transitions.clear(); - } - - } - - @SuppressWarnings("unchecked") - @BeforeEach - public void setup() { - context = new AnnotationConfigApplicationContext(); - context.register(CommonConfiguration.class, Application.class, Config.class); - context.refresh(); - machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); - listener = context.getBean(TestListener.class); - doStartAndAssert(machine); - } - - @AfterEach - public void clean() { - doStopAndAssert(machine); - context.close(); - context = null; - machine = null; - } - -} diff --git a/spring-statemachine-samples/spring-statemachine-samples.gradle b/spring-statemachine-samples/spring-statemachine-samples.gradle index d955870e6..023055970 100644 --- a/spring-statemachine-samples/spring-statemachine-samples.gradle +++ b/spring-statemachine-samples/spring-statemachine-samples.gradle @@ -7,7 +7,6 @@ description = 'Spring State Machine Samples Common' dependencies { management platform(project(":spring-statemachine-platform")) implementation project(':spring-statemachine-core') - implementation 'org.springframework.shell:spring-shell-core' implementation 'org.springframework.boot:spring-boot-starter' } diff --git a/spring-statemachine-samples/src/main/java/demo/AbstractStateMachineCommands.java b/spring-statemachine-samples/src/main/java/demo/AbstractStateMachineCommands.java index f2ea1580d..a18cbe1b2 100644 --- a/spring-statemachine-samples/src/main/java/demo/AbstractStateMachineCommands.java +++ b/spring-statemachine-samples/src/main/java/demo/AbstractStateMachineCommands.java @@ -15,18 +15,21 @@ */ package demo; -import java.util.Iterator; -import java.util.Map.Entry; -import java.util.Set; - import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.shell.command.annotation.Command; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.state.State; import org.springframework.util.StringUtils; -@Command +import java.io.InputStream; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Scanner; +import java.util.Set; + +@Configuration public class AbstractStateMachineCommands { @Autowired @@ -36,54 +39,74 @@ protected StateMachine getStateMachine() { return stateMachine; } - @Autowired - @Qualifier("stateChartModel") - private String stateChartModel; - - @Command(command = "sm state", description = "Prints current state") - public String state() { - State state = stateMachine.getState(); - if (state != null) { - return StringUtils.collectionToCommaDelimitedString(state.getIds()); - } else { - return "No state"; - } + @Bean + public Command state() { + return new BasicCommand("state", "Prints current state") { + public String execute(String[] args) { + State state = stateMachine.getState(); + if (state != null) { + return StringUtils.collectionToCommaDelimitedString(state.getIds()); + } else { + return "No state"; + } + } + }; } - @Command(command = "sm start", description = "Start a state machine") - public String start() { - stateMachine.startReactively().subscribe(); - return "State machine started"; + @Bean + public Command start() { + return new BasicCommand("start", "Start a state machine") { + public String execute(String[] args) { + stateMachine.startReactively().subscribe(); + return "State machine started"; + } + }; } - @Command(command = "sm stop", description = "Stop a state machine") - public String stop() { - stateMachine.stopReactively().subscribe(); - return "State machine stopped"; + @Bean + public Command stop() { + return new BasicCommand("stop", "Stop a state machine") { + public String execute(String[] args) { + stateMachine.stopReactively().subscribe(); + return "State machine stopped"; + } + }; } - @Command(command = "sm print", description = "Print state machine") - public String print() { - return stateChartModel; + @Bean + public Command print() { + return new BasicCommand("print", "Print state machine") { + public String execute(String[] args) throws Exception { + ClassPathResource model = new ClassPathResource("statechartmodel.txt"); + InputStream inputStream = model.getInputStream(); + Scanner scanner = new Scanner(inputStream); + String content = scanner.useDelimiter("\\Z").next(); + scanner.close(); + return content; + } + }; } - @Command(command = "sm variables", description = "Prints extended state variables") - public String variables() { - StringBuilder buf = new StringBuilder(); - Set> entrySet = stateMachine.getExtendedState().getVariables().entrySet(); - Iterator> iterator = entrySet.iterator(); - if (entrySet.size() > 0) { - while (iterator.hasNext()) { - Entry e = iterator.next(); - buf.append(e.getKey() + "=" + e.getValue()); - if (iterator.hasNext()) { - buf.append("\n"); + @Bean + public Command variables() { + return new BasicCommand("variables", "Prints extended state variables") { + public String execute(String[] args) { + StringBuilder buf = new StringBuilder(); + Set> entrySet = stateMachine.getExtendedState().getVariables().entrySet(); + Iterator> iterator = entrySet.iterator(); + if (entrySet.size() > 0) { + while (iterator.hasNext()) { + Entry e = iterator.next(); + buf.append(e.getKey() + "=" + e.getValue()); + if (iterator.hasNext()) { + buf.append("\n"); + } + } + } else { + buf.append("No variables"); } + return buf.toString(); } - } else { - buf.append("No variables"); - } - return buf.toString(); + }; } - -} \ No newline at end of file +} diff --git a/spring-statemachine-samples/src/main/java/demo/BasicCommand.java b/spring-statemachine-samples/src/main/java/demo/BasicCommand.java new file mode 100644 index 000000000..4f9be5099 --- /dev/null +++ b/spring-statemachine-samples/src/main/java/demo/BasicCommand.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package demo; + +/** + * Basic implementation of the {@link Command} interface. + * This class provides a simple way to create commands with a name and description. + * It can be extended to implement specific command logic. + * + * @author Mahmoud Ben Hassine + */ +public class BasicCommand implements Command { + + private final String name; + private final String description; + + /** + * Create a new {@link BasicCommand} with a name and description. + * + * @param name the command name + * @param description the command description + */ + public BasicCommand(String name, String description) { + this.name = name; + this.description = description; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String execute(String[] args) throws Exception { + throw new UnsupportedOperationException("Not implemented yet"); + } +} diff --git a/spring-statemachine-samples/src/main/java/demo/StateMachinePromptProvider.java b/spring-statemachine-samples/src/main/java/demo/Command.java similarity index 53% rename from spring-statemachine-samples/src/main/java/demo/StateMachinePromptProvider.java rename to spring-statemachine-samples/src/main/java/demo/Command.java index 897e02990..cc0b2585c 100644 --- a/spring-statemachine-samples/src/main/java/demo/StateMachinePromptProvider.java +++ b/spring-statemachine-samples/src/main/java/demo/Command.java @@ -15,19 +15,31 @@ */ package demo; -import org.jline.utils.AttributedString; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.shell.jline.PromptProvider; -import org.springframework.stereotype.Component; +/** + * Simple interface for command line commands. + * + * @author Mahmoud Ben Hassine + */ +public interface Command { + + /** + * The command name. + * @return the command name + */ + String getName(); -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) -public class StateMachinePromptProvider implements PromptProvider { + /** + * The command description. + * @return the command description + */ + String getDescription(); - @Override - public AttributedString getPrompt() { - return AttributedString.fromAnsi("sm>"); - } + /** + * Execute the command. + * @param args the command arguments + * @return the message to be printed to the console + * @throws Exception in case of error + */ + String execute(String[] args) throws Exception; } diff --git a/spring-statemachine-samples/src/main/java/demo/CommandRunner.java b/spring-statemachine-samples/src/main/java/demo/CommandRunner.java new file mode 100644 index 000000000..dd6f7dc32 --- /dev/null +++ b/spring-statemachine-samples/src/main/java/demo/CommandRunner.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package demo; + +import org.springframework.beans.BeansException; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; + +import java.io.Console; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Command line runner that executes commands. + *

+ * This class implements the {@link ApplicationRunner} interface and is responsible for + * executing commands in the command line interface. It retrieves all beans of type {@link Command} + * from the application context and allows the user to execute them interactively. + * + * @author Mahmoud Ben Hassine + */ +@Component +public class CommandRunner implements ApplicationRunner, ApplicationContextAware, Ordered { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + Map commands = this.applicationContext.getBeansOfType(Command.class); + Console console = System.console(); + if (console == null) { + System.err.println("No console available."); + return; + } + + final String lineSeparator = System.lineSeparator(); + console.printf("Available commands:" + lineSeparator); + for (Command command : commands.values()) { + console.printf(" " + command.getName() + ": " + command.getDescription() + lineSeparator); + } + console.printf(" quit: exit" + lineSeparator); + + String commandString = ""; + while (!commandString.equalsIgnoreCase("quit")) { + console.printf("sm>"); + commandString = console.readLine(); + String[] tokens = commandString.split(" "); + String commandName = tokens[0]; + String[] commandArgs = Stream.of(tokens).skip(1).limit(tokens.length).toArray(String[]::new); + if (commandName.equalsIgnoreCase("quit")) { + continue; + } + try { + Command command = commands.get(commandName); + if (command == null) { + console.printf("Command not found: " + commandString + lineSeparator); + continue; + } + console.printf(command.execute(commandArgs)); + console.printf(lineSeparator); + } catch (Exception exception) { + console.printf("Error while executing command: " + commandString + lineSeparator); + exception.printStackTrace(); + } + } + console.printf("bye!" + lineSeparator); + } + +} diff --git a/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java b/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java deleted file mode 100644 index d2b698434..000000000 --- a/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package demo; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Scanner; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.statemachine.event.OnStateEntryEvent; -import org.springframework.statemachine.event.OnStateExitEvent; -import org.springframework.statemachine.event.OnTransitionEvent; -import org.springframework.statemachine.event.StateMachineEvent; -import org.springframework.statemachine.transition.TransitionKind; - -@Configuration -public class CommonConfiguration { - - private final static Log log = LogFactory.getLog(CommonConfiguration.class); - - @Configuration - static class ApplicationConfig { - - @Bean - public TestEventListener testEventListener() { - return new TestEventListener(); - } - - @Bean - public String stateChartModel() throws IOException { - ClassPathResource model = new ClassPathResource("statechartmodel.txt"); - InputStream inputStream = model.getInputStream(); - Scanner scanner = new Scanner(inputStream); - String content = scanner.useDelimiter("\\Z").next(); - scanner.close(); - return content; - } - - } - - static class TestEventListener implements ApplicationListener { - - @Override - public void onApplicationEvent(StateMachineEvent event) { - if (event instanceof OnStateEntryEvent) { - OnStateEntryEvent e = (OnStateEntryEvent)event; - log.info("Entry state " + e.getState().getId()); - } else if (event instanceof OnStateExitEvent) { - OnStateExitEvent e = (OnStateExitEvent)event; - log.info("Exit state " + e.getState().getId()); - } else if (event instanceof OnTransitionEvent) { - OnTransitionEvent e = (OnTransitionEvent)event; - if (e.getTransition().getKind() == TransitionKind.INTERNAL) { - log.info("Internal transition source=" + e.getTransition().getSource().getId()); - } - } - } - - } - -} diff --git a/spring-statemachine-samples/src/main/java/demo/StateMachineEventListener.java b/spring-statemachine-samples/src/main/java/demo/StateMachineEventListener.java new file mode 100644 index 000000000..3f819c21b --- /dev/null +++ b/spring-statemachine-samples/src/main/java/demo/StateMachineEventListener.java @@ -0,0 +1,31 @@ +package demo; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.statemachine.event.OnStateEntryEvent; +import org.springframework.statemachine.event.OnStateExitEvent; +import org.springframework.statemachine.event.OnTransitionEvent; +import org.springframework.statemachine.event.StateMachineEvent; +import org.springframework.statemachine.transition.TransitionKind; +import org.springframework.stereotype.Component; + +@Component +class StateMachineEventListener implements ApplicationListener { + + private final static Log log = LogFactory.getLog(StateMachineEventListener.class); + + @Override + public void onApplicationEvent(StateMachineEvent stateMachineEvent) { + if (stateMachineEvent instanceof OnStateEntryEvent event) { + log.info("Entry state " + event.getState().getId()); + } else if (stateMachineEvent instanceof OnStateExitEvent event) { + log.info("Exit state " + event.getState().getId()); + } else if (stateMachineEvent instanceof OnTransitionEvent event) { + if (event.getTransition().getKind() == TransitionKind.INTERNAL) { + log.info("Internal transition source=" + event.getTransition().getSource().getId()); + } + } + } + +} \ No newline at end of file diff --git a/spring-statemachine-samples/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/src/main/resources/META-INF/spring/spring-shell-plugin.xml deleted file mode 100644 index 1112bb354..000000000 --- a/spring-statemachine-samples/src/main/resources/META-INF/spring/spring-shell-plugin.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/spring-statemachine-samples/tasks/spring-statemachine-samples-tasks.gradle b/spring-statemachine-samples/tasks/spring-statemachine-samples-tasks.gradle index 37d61a407..70ba6fc1e 100644 --- a/spring-statemachine-samples/tasks/spring-statemachine-samples-tasks.gradle +++ b/spring-statemachine-samples/tasks/spring-statemachine-samples-tasks.gradle @@ -8,7 +8,7 @@ dependencies { management platform(project(":spring-statemachine-platform")) implementation project(':spring-statemachine-samples-common') implementation project(':spring-statemachine-core') - implementation 'org.springframework.shell:spring-shell-core' + implementation 'org.springframework.boot:spring-boot-starter' testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation (project(':spring-statemachine-test')) testImplementation 'org.hamcrest:hamcrest-core' @@ -17,7 +17,7 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' testImplementation 'io.projectreactor.tools:blockhound' } diff --git a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java index ea9dc3d6b..080a714f6 100644 --- a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; @@ -39,7 +40,7 @@ import reactor.core.publisher.Mono; -@Configuration +@SpringBootApplication(scanBasePackages = "demo") public class Application { @Configuration diff --git a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/StateMachineCommands.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/StateMachineCommands.java index 90fa20cbb..f8056aba8 100644 --- a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/StateMachineCommands.java +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/StateMachineCommands.java @@ -15,24 +15,32 @@ */ package demo.tasks; +import demo.BasicCommand; +import demo.Command; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; import demo.AbstractStateMachineCommands; import demo.tasks.Application.Events; import demo.tasks.Application.States; import reactor.core.publisher.Mono; -@Command +@Configuration public class StateMachineCommands extends AbstractStateMachineCommands { - @Command(command = "sm event", description = "Sends an event to a state machine") - public String event(@Option(longNames = { "", "event" }, required = true, description = "The event") final Events event) { - getStateMachine() - .sendEvent(Mono.just(MessageBuilder - .withPayload(event).build())) - .subscribe(); - return "Event " + event + " send"; + @Bean + public Command event() { + return new BasicCommand("event", "Sends an event to a state machine") { + @Override + public String execute(String[] args) { + Events event = Events.valueOf(args[0]); + getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(event).build())) + .subscribe(); + return "Event " + event + " sent"; + } + }; } -} +} \ No newline at end of file diff --git a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/TasksCommands.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/TasksCommands.java index b23a46f0b..d04d12f33 100644 --- a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/TasksCommands.java +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/TasksCommands.java @@ -15,34 +15,60 @@ */ package demo.tasks; +import demo.BasicCommand; +import demo.Command; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; -@Command +@Configuration public class TasksCommands { @Autowired private Tasks tasks; - @Command(command = "tasks run", description = "Run tasks") - public void run() { - tasks.run(); + @Bean + public Command run() { + return new BasicCommand("run", "Run tasks") { + @Override + public String execute(String[] args) { + tasks.run(); + return "Tasks started"; + } + }; } - @Command(command = "tasks list", description = "List tasks") - public String list() { - return tasks.toString(); + @Bean + public Command list() { + return new BasicCommand("list", "List tasks") { + @Override + public String execute(String[] args) { + return tasks.toString(); + } + }; } - @Command(command = "tasks fix", description = "Fix tasks") - public void fix() { - tasks.fix(); + @Bean + public Command fix() { + return new BasicCommand("fix", "Fix tasks") { + @Override + public String execute(String[] args) { + tasks.fix(); + return "Tasks fixed"; + } + }; } - @Command(command = "tasks fail", description = "Fail task") - public void fail(@Option(longNames = {"", "task"}, description = "Task id") String task) { - tasks.fail(task); + @Bean + public Command fail() { + return new BasicCommand("fail [taskId]", "Fail task with [taskId]") { + @Override + public String execute(String[] args) { + String taskId = args[0]; + tasks.fail(taskId); + return "Task " + taskId + " failed"; + } + }; } } diff --git a/spring-statemachine-samples/tasks/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/tasks/src/main/resources/META-INF/spring/spring-shell-plugin.xml deleted file mode 100644 index 1112bb354..000000000 --- a/spring-statemachine-samples/tasks/src/main/resources/META-INF/spring/spring-shell-plugin.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/spring-statemachine-samples/tasks/src/main/resources/application.properties b/spring-statemachine-samples/tasks/src/main/resources/application.properties new file mode 100644 index 000000000..709574239 --- /dev/null +++ b/spring-statemachine-samples/tasks/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/StateMachineBlockHoundIntegration.java b/spring-statemachine-samples/tasks/src/test/java/demo/tasks/StateMachineBlockHoundIntegration.java deleted file mode 100644 index a41363a4a..000000000 --- a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/StateMachineBlockHoundIntegration.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package demo.tasks; - -import reactor.blockhound.BlockHound.Builder; -import reactor.blockhound.integration.BlockHoundIntegration; - -public class StateMachineBlockHoundIntegration implements BlockHoundIntegration { - - @Override - public void applyTo(Builder builder) { - // whitelisting some blocking calls in tests - builder - .allowBlockingCallsInside("demo.tasks.Tasks", "sleep") - .allowBlockingCallsInside("java.util.concurrent.locks.LockSupport", "park"); - } -} diff --git a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java b/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java deleted file mode 100644 index 08669f964..000000000 --- a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package demo.tasks; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.statemachine.TestUtils.doStartAndAssert; -import static org.springframework.statemachine.TestUtils.doStopAndAssert; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.statemachine.ObjectStateMachine; -import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.StateMachineSystemConstants; -import org.springframework.statemachine.listener.StateMachineListener; -import org.springframework.statemachine.listener.StateMachineListenerAdapter; -import org.springframework.statemachine.state.State; -import org.springframework.statemachine.transition.Transition; -import org.springframework.util.StringUtils; - -import demo.CommonConfiguration; -import demo.tasks.Application.Events; -import demo.tasks.Application.States; - -public class TasksTests { - - private final static Log log = LogFactory.getLog(TasksTests.class); - - private AnnotationConfigApplicationContext context; - - private StateMachine machine; - - private Tasks tasks; - - private TestListener listener; - - @Test - public void testInitialState() throws InterruptedException { - Map variables = machine.getExtendedState().getVariables(); - assertThat(variables).isEmpty(); - } - - @Test - public void testRunOnce() throws InterruptedException { - listener.reset(8, 8, 0); - tasks.run(); - assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly(States.READY); - Map variables = machine.getExtendedState().getVariables(); - assertThat(variables).hasSize(3); - } - - @Test - public void testRunTwice() throws InterruptedException { - listener.reset(8, 8, 0); - tasks.run(); - assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly(States.READY); - - Map variables = machine.getExtendedState().getVariables(); - assertThat(variables).hasSize(3); - - listener.reset(8, 8, 0); - tasks.run(); - assertThat(listener.stateEnteredLatch.await(8, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly(States.READY); - - variables = machine.getExtendedState().getVariables(); - assertThat(variables).hasSize(3); - } - - @Test - @Tag("smoke") - public void testRunSmoke() throws InterruptedException { - for (int i = 0; i < 20; i++) { - log.info("testRunSmoke SMOKE START " + i); - listener.reset(8, 8, 0); - tasks.run(); - - boolean await = listener.stateEnteredLatch.await(8, TimeUnit.SECONDS); - String reason = "Machine was " + machine + " " + StringUtils.collectionToCommaDelimitedString(listener.statesEntered); - assertThat(await).isTrue().withFailMessage(reason); - assertThat(machine.getState().getIds()).containsExactly(States.READY); - log.info("testRunSmoke SMOKE STOP " + i); - } - } - - @Test - public void testFailAutomaticFix() throws InterruptedException { - listener.reset(10, 0, 0); - tasks.fail("T1"); - tasks.run(); - assertThat(listener.stateChangedLatch.await(6, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(10); - assertThat(machine.getState().getIds()).containsExactly(States.READY); - } - - @Test - public void testFailManualFix() throws InterruptedException { - listener.reset(10, 0, 0); - tasks.fail("T2"); - tasks.run(); - assertThat(listener.stateChangedLatch.await(6, TimeUnit.SECONDS)).isTrue(); - - Map variables = machine.getExtendedState().getVariables(); - assertThat(variables).hasSize(3); - - assertThat(machine.getState().getIds()).containsExactly(States.ERROR, States.MANUAL); - listener.reset(1, 0, 0); - tasks.fix(); - assertThat(listener.stateChangedLatch.await(6, TimeUnit.SECONDS)).isTrue(); - assertThat(machine.getState().getIds()).containsExactly(States.READY); - } - - @SuppressWarnings("unchecked") - @BeforeEach - public void setup() throws Exception { - context = new AnnotationConfigApplicationContext(); - context.register(CommonConfiguration.class, Application.class, TestConfig.class); - context.refresh(); - machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); - tasks = context.getBean(Tasks.class); - listener = context.getBean(TestListener.class); - doStartAndAssert(machine); - assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS)).isTrue(); - assertThat(listener.stateChangedCount).isEqualTo(1); - assertThat(machine.getState().getIds()).containsExactly(States.READY); - } - - @AfterEach - public void clean() { - doStopAndAssert(machine); - context.close(); - context = null; - machine = null; - tasks = null; - listener = null; - } - - static class TestConfig { - - @Autowired - private StateMachine machine; - - @Bean - public StateMachineListener stateMachineListener() { - TestListener listener = new TestListener(); - machine.addStateListener(listener); - return listener; - } - - } - - static class TestListener extends StateMachineListenerAdapter { - - final Object lock = new Object(); - - volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); - volatile CountDownLatch stateEnteredLatch = new CountDownLatch(2); - volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); - volatile CountDownLatch transitionLatch = new CountDownLatch(0); - volatile int stateChangedCount = 0; - volatile int transitionCount = 0; - List> statesEntered = new ArrayList>(); - List> statesExited = new ArrayList>(); - - @Override - public void stateChanged(State from, State to) { - synchronized (lock) { - stateChangedCount++; - stateChangedLatch.countDown(); - } - } - - @Override - public void stateEntered(State state) { - synchronized (lock) { - statesEntered.add(state); - stateEnteredLatch.countDown(); - } - } - - @Override - public void stateExited(State state) { - synchronized (lock) { - statesExited.add(state); - stateExitedLatch.countDown(); - } - } - - @Override - public void transitionEnded(Transition transition) { - synchronized (lock) { - transitionCount++; - transitionLatch.countDown(); - } - } - - public void reset(int c1, int c2, int c3) { - reset(c1, c2, c3, 0); - } - - public void reset(int c1, int c2, int c3, int c4) { - synchronized (lock) { - stateChangedLatch = new CountDownLatch(c1); - stateEnteredLatch = new CountDownLatch(c2); - stateExitedLatch = new CountDownLatch(c3); - transitionLatch = new CountDownLatch(c4); - stateChangedCount = 0; - transitionCount = 0; - statesEntered.clear(); - statesExited.clear(); - } - } - - } - -} diff --git a/spring-statemachine-samples/tasks/src/test/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration b/spring-statemachine-samples/tasks/src/test/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration deleted file mode 100644 index a487938c2..000000000 --- a/spring-statemachine-samples/tasks/src/test/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration +++ /dev/null @@ -1 +0,0 @@ -demo.tasks.StateMachineBlockHoundIntegration \ No newline at end of file diff --git a/spring-statemachine-samples/tasks/src/test/resources/log4j2.xml b/spring-statemachine-samples/tasks/src/test/resources/log4j2.xml deleted file mode 100644 index 9c440f57c..000000000 --- a/spring-statemachine-samples/tasks/src/test/resources/log4j2.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/spring-statemachine-samples/tasks/src/test/resources/logback.xml b/spring-statemachine-samples/tasks/src/test/resources/logback.xml deleted file mode 100644 index cb6447aa5..000000000 --- a/spring-statemachine-samples/tasks/src/test/resources/logback.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - %d{yyyy-MM-dd HH:mm:ss} %t %m%n - utf8 - - - - - - - - - - diff --git a/spring-statemachine-samples/turnstile/spring-statemachine-samples-turnstile.gradle b/spring-statemachine-samples/turnstile/spring-statemachine-samples-turnstile.gradle index 5c1c1d380..709b16a1b 100644 --- a/spring-statemachine-samples/turnstile/spring-statemachine-samples-turnstile.gradle +++ b/spring-statemachine-samples/turnstile/spring-statemachine-samples-turnstile.gradle @@ -8,7 +8,7 @@ dependencies { management platform(project(":spring-statemachine-platform")) implementation project(':spring-statemachine-samples-common') implementation project(':spring-statemachine-core') - implementation 'org.springframework.shell:spring-shell-starter' + implementation 'org.springframework.boot:spring-boot-starter' testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation (project(':spring-statemachine-test')) testImplementation 'org.hamcrest:hamcrest-core' @@ -17,6 +17,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/Application.java b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/Application.java index 3cc20e235..9a58c2f40 100644 --- a/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/Application.java +++ b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/Application.java @@ -15,66 +15,12 @@ */ package demo.turnstile; -import java.util.EnumSet; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Configuration; -import org.springframework.shell.command.annotation.CommandScan; -import org.springframework.statemachine.config.EnableStateMachine; -import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; -import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; -import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; -@CommandScan @SpringBootApplication(scanBasePackages = "demo") public class Application { -//tag::snippetA[] - @Configuration - @EnableStateMachine - static class StateMachineConfig - extends EnumStateMachineConfigurerAdapter { - - @Override - public void configure(StateMachineStateConfigurer states) - throws Exception { - states - .withStates() - .initial(States.LOCKED) - .states(EnumSet.allOf(States.class)); - } - - @Override - public void configure(StateMachineTransitionConfigurer transitions) - throws Exception { - transitions - .withExternal() - .source(States.LOCKED) - .target(States.UNLOCKED) - .event(Events.COIN) - .and() - .withExternal() - .source(States.UNLOCKED) - .target(States.LOCKED) - .event(Events.PUSH); - } - - } -//end::snippetA[] - -//tag::snippetB[] - public enum States { - LOCKED, UNLOCKED - } -//end::snippetB[] - -//tag::snippetC[] - public enum Events { - COIN, PUSH - } -//end::snippetC[] - public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/Events.java b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/Events.java new file mode 100644 index 000000000..363aa6d16 --- /dev/null +++ b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/Events.java @@ -0,0 +1,7 @@ +package demo.turnstile; + +//tag::snippetC[] +public enum Events { + COIN, PUSH +} +//end::snippetC[] \ No newline at end of file diff --git a/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/StateMachineCommands.java b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/StateMachineCommands.java index f02e1f50f..58d45dbbe 100644 --- a/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/StateMachineCommands.java +++ b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/StateMachineCommands.java @@ -15,24 +15,30 @@ */ package demo.turnstile; +import demo.BasicCommand; +import demo.Command; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; import demo.AbstractStateMachineCommands; -import demo.turnstile.Application.Events; -import demo.turnstile.Application.States; import reactor.core.publisher.Mono; -@Command +@Configuration public class StateMachineCommands extends AbstractStateMachineCommands { - @Command(command = "sm event", description = "Sends an event to a state machine") - public String event(@Option(longNames = { "", "event" }, required = true, description = "The event") final Events event) { - getStateMachine() - .sendEvent(Mono.just(MessageBuilder - .withPayload(event).build())) - .subscribe(); - return "Event " + event + " send"; + @Bean + public Command event() { + return new BasicCommand("event", "Sends an event to a state machine") { + @Override + public String execute(String[] args) { + Events event = Events.valueOf(args[0]); + getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(event).build())) + .subscribe(); + return "Event " + event + " sent"; + } + }; } } diff --git a/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/StateMachineConfiguration.java b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/StateMachineConfiguration.java new file mode 100644 index 000000000..28ec441f4 --- /dev/null +++ b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/StateMachineConfiguration.java @@ -0,0 +1,42 @@ +package demo.turnstile; + +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +import java.util.EnumSet; + +//tag::snippetA[] +@Configuration +@EnableStateMachine +public class StateMachineConfiguration + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.LOCKED) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.LOCKED) + .target(States.UNLOCKED) + .event(Events.COIN) + .and() + .withExternal() + .source(States.UNLOCKED) + .target(States.LOCKED) + .event(Events.PUSH); + } + +} +//end::snippetA[] diff --git a/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/States.java b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/States.java new file mode 100644 index 000000000..9f97acdcd --- /dev/null +++ b/spring-statemachine-samples/turnstile/src/main/java/demo/turnstile/States.java @@ -0,0 +1,7 @@ +package demo.turnstile; + +//tag::snippetB[] +public enum States { + LOCKED, UNLOCKED +} +//end::snippetB[] \ No newline at end of file diff --git a/spring-statemachine-samples/turnstile/src/main/resources/application.properties b/spring-statemachine-samples/turnstile/src/main/resources/application.properties index 810a40c47..709574239 100644 --- a/spring-statemachine-samples/turnstile/src/main/resources/application.properties +++ b/spring-statemachine-samples/turnstile/src/main/resources/application.properties @@ -1 +1 @@ -spring.shell.interactive.enabled=true +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/spring-statemachine-samples/turnstile/src/test/java/demo/turnstile/TurnstileTests.java b/spring-statemachine-samples/turnstile/src/test/java/demo/turnstile/TurnstileTests.java deleted file mode 100644 index e3976f308..000000000 --- a/spring-statemachine-samples/turnstile/src/test/java/demo/turnstile/TurnstileTests.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package demo.turnstile; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.statemachine.TestUtils.doStartAndAssert; -import static org.springframework.statemachine.TestUtils.doStopAndAssert; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.statemachine.ObjectStateMachine; -import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.StateMachineSystemConstants; -import org.springframework.statemachine.listener.StateMachineListener; -import org.springframework.statemachine.listener.StateMachineListenerAdapter; -import org.springframework.statemachine.state.State; -import org.springframework.statemachine.transition.Transition; - -import demo.CommonConfiguration; -import demo.turnstile.Application.Events; -import demo.turnstile.Application.States; - -public class TurnstileTests { - - private AnnotationConfigApplicationContext context; - - private StateMachine machine; - - private TestListener listener; - - private StateMachineCommands commands; - - @Test - public void testNotStarted() throws Exception { - assertThat(commands.state()).isEqualTo("No state"); - } - - @Test - public void testInitialState() throws Exception { - doStartAndAssert(machine); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.LOCKED); - assertThat(listener.statesEntered).hasSize(1); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.LOCKED); - assertThat(listener.statesExited).isEmpty(); - } - - static class Config { - - @Autowired - private StateMachine machine; - - @Bean - public StateMachineListener stateMachineListener() { - TestListener listener = new TestListener(); - machine.addStateListener(listener); - return listener; - } - } - - static class TestListener extends StateMachineListenerAdapter { - - volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); - volatile CountDownLatch stateEnteredLatch = new CountDownLatch(2); - volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); - volatile CountDownLatch transitionLatch = new CountDownLatch(0); - volatile List> transitions = new ArrayList>(); - List> statesEntered = new ArrayList>(); - List> statesExited = new ArrayList>(); - volatile int transitionCount = 0; - - @Override - public void stateChanged(State from, State to) { - stateChangedLatch.countDown(); - } - - @Override - public void stateEntered(State state) { - statesEntered.add(state); - stateEnteredLatch.countDown(); - } - - @Override - public void stateExited(State state) { - statesExited.add(state); - stateExitedLatch.countDown(); - } - - @Override - public void transition(Transition transition) { - transitions.add(transition); - transitionLatch.countDown(); - transitionCount++; - } - - public void reset(int c1, int c2, int c3) { - reset(c1, c2, c3, 0); - } - - public void reset(int c1, int c2, int c3, int c4) { - stateChangedLatch = new CountDownLatch(c1); - stateEnteredLatch = new CountDownLatch(c2); - stateExitedLatch = new CountDownLatch(c3); - transitionLatch = new CountDownLatch(c4); - statesEntered.clear(); - statesExited.clear(); - transitionCount = 0; - transitions.clear(); - } - - } - - @SuppressWarnings("unchecked") - @BeforeEach - public void setup() { - context = new AnnotationConfigApplicationContext(); - context.register(CommonConfiguration.class, Application.class, Config.class); - context.refresh(); - machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); - listener = context.getBean(TestListener.class); - commands = context.getBean(StateMachineCommands.class); - } - - @AfterEach - public void clean() { - doStopAndAssert(machine); - context.close(); - context = null; - machine = null; - } - -} diff --git a/spring-statemachine-samples/turnstilereactive/spring-statemachine-samples-turnstilereactive.gradle b/spring-statemachine-samples/turnstilereactive/spring-statemachine-samples-turnstilereactive.gradle index cc18f48ba..cd224e485 100644 --- a/spring-statemachine-samples/turnstilereactive/spring-statemachine-samples-turnstilereactive.gradle +++ b/spring-statemachine-samples/turnstilereactive/spring-statemachine-samples-turnstilereactive.gradle @@ -20,7 +20,7 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' testImplementation 'io.projectreactor.tools:blockhound' } diff --git a/spring-statemachine-samples/washer/spring-statemachine-samples-washer.gradle b/spring-statemachine-samples/washer/spring-statemachine-samples-washer.gradle index 88389f411..51aef656b 100644 --- a/spring-statemachine-samples/washer/spring-statemachine-samples-washer.gradle +++ b/spring-statemachine-samples/washer/spring-statemachine-samples-washer.gradle @@ -8,7 +8,7 @@ dependencies { management platform(project(":spring-statemachine-platform")) implementation project(':spring-statemachine-samples-common') implementation project(':spring-statemachine-core') - implementation 'org.springframework.shell:spring-shell-core' + implementation 'org.springframework.boot:spring-boot-starter' testImplementation(testFixtures(project(':spring-statemachine-core'))) testImplementation (project(':spring-statemachine-test')) testImplementation 'org.hamcrest:hamcrest-core' @@ -17,6 +17,6 @@ dependencies { testImplementation 'org.springframework:spring-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java b/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java index b71a78ca4..4e0d19895 100644 --- a/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java +++ b/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java @@ -16,6 +16,7 @@ package demo.washer; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; @@ -23,7 +24,7 @@ import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.config.configurers.StateConfigurer.History; -@Configuration +@SpringBootApplication(scanBasePackages = "demo") public class Application { @Configuration diff --git a/spring-statemachine-samples/washer/src/main/java/demo/washer/StateMachineCommands.java b/spring-statemachine-samples/washer/src/main/java/demo/washer/StateMachineCommands.java index 4eb90960b..1b98041f6 100644 --- a/spring-statemachine-samples/washer/src/main/java/demo/washer/StateMachineCommands.java +++ b/spring-statemachine-samples/washer/src/main/java/demo/washer/StateMachineCommands.java @@ -15,25 +15,32 @@ */ package demo.washer; +import demo.BasicCommand; +import demo.Command; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; import demo.AbstractStateMachineCommands; import demo.washer.Application.Events; import demo.washer.Application.States; import reactor.core.publisher.Mono; -@Command +@Configuration public class StateMachineCommands extends AbstractStateMachineCommands { - @Command(command = "sm event", description = "Sends an event to a state machine") - public String event(@Option(longNames = { "", "event" }, required = true, description = "The event") final Events event) { - getStateMachine() - .sendEvent(Mono.just(MessageBuilder - .withPayload(event).build())) - .subscribe(); - return "Event " + event + " send"; + @Bean + public Command event() { + return new BasicCommand("event", "Sends an event to a state machine") { + @Override + public String execute(String[] args) { + Events event = Events.valueOf(args[0]); + getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(event).build())) + .subscribe(); + return "Event " + event + " sent"; + } + }; } - } \ No newline at end of file diff --git a/spring-statemachine-samples/washer/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/washer/src/main/resources/META-INF/spring/spring-shell-plugin.xml deleted file mode 100644 index 1112bb354..000000000 --- a/spring-statemachine-samples/washer/src/main/resources/META-INF/spring/spring-shell-plugin.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/spring-statemachine-samples/washer/src/main/resources/application.properties b/spring-statemachine-samples/washer/src/main/resources/application.properties new file mode 100644 index 000000000..709574239 --- /dev/null +++ b/spring-statemachine-samples/washer/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/spring-statemachine-samples/washer/src/test/java/demo/washer/WasherTests.java b/spring-statemachine-samples/washer/src/test/java/demo/washer/WasherTests.java deleted file mode 100644 index 893cea5a0..000000000 --- a/spring-statemachine-samples/washer/src/test/java/demo/washer/WasherTests.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package demo.washer; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.statemachine.TestUtils.doSendEventAndConsumeAll; -import static org.springframework.statemachine.TestUtils.doStartAndAssert; -import static org.springframework.statemachine.TestUtils.doStopAndAssert; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.statemachine.ObjectStateMachine; -import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.StateMachineSystemConstants; -import org.springframework.statemachine.listener.StateMachineListener; -import org.springframework.statemachine.listener.StateMachineListenerAdapter; -import org.springframework.statemachine.state.State; -import org.springframework.statemachine.transition.Transition; - -import demo.CommonConfiguration; -import demo.washer.Application.Events; -import demo.washer.Application.States; - -public class WasherTests { - - private AnnotationConfigApplicationContext context; - - private StateMachine machine; - - private TestListener listener; - - @Test - public void testInitialState() throws Exception { - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.RUNNING, States.WASHING); - assertThat(listener.statesEntered).hasSize(2); - assertThat(listener.statesEntered.get(0).getId()).isEqualTo(States.RUNNING); - assertThat(listener.statesEntered.get(1).getId()).isEqualTo(States.WASHING); - assertThat(listener.statesExited).isEmpty(); - } - - @Test - public void testRinse() throws Exception { - listener.reset(1, 0, 0); - doSendEventAndConsumeAll(machine, Events.RINSE); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.RUNNING, States.RINSING); - } - - @Test - public void testRinseCutPower() throws Exception { - listener.reset(1, 0, 0); - doSendEventAndConsumeAll(machine, Events.RINSE); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - - listener.reset(1, 0, 0); - doSendEventAndConsumeAll(machine, Events.CUTPOWER); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.POWEROFF); - } - - @Test - public void testRinseCutRestorePower() throws Exception { - listener.reset(1, 0, 0); - doSendEventAndConsumeAll(machine, Events.RINSE); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - - listener.reset(1, 0, 0); - doSendEventAndConsumeAll(machine, Events.CUTPOWER); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - - listener.reset(1, 0, 0); - doSendEventAndConsumeAll(machine, Events.RESTOREPOWER); - listener.stateChangedLatch.await(1, TimeUnit.SECONDS); - assertThat(machine.getState().getIds()).containsExactly(States.RUNNING, States.RINSING); - } - - static class Config { - - @Autowired - private StateMachine machine; - - @Bean - public StateMachineListener stateMachineListener() { - TestListener listener = new TestListener(); - machine.addStateListener(listener); - return listener; - } - } - - static class TestListener extends StateMachineListenerAdapter { - - volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); - volatile CountDownLatch stateEnteredLatch = new CountDownLatch(2); - volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); - volatile CountDownLatch transitionLatch = new CountDownLatch(0); - volatile List> transitions = new ArrayList>(); - List> statesEntered = new ArrayList>(); - List> statesExited = new ArrayList>(); - volatile int transitionCount = 0; - - @Override - public void stateChanged(State from, State to) { - stateChangedLatch.countDown(); - } - - @Override - public void stateEntered(State state) { - statesEntered.add(state); - stateEnteredLatch.countDown(); - } - - @Override - public void stateExited(State state) { - statesExited.add(state); - stateExitedLatch.countDown(); - } - - @Override - public void transition(Transition transition) { - transitions.add(transition); - transitionLatch.countDown(); - transitionCount++; - } - - public void reset(int c1, int c2, int c3) { - reset(c1, c2, c3, 0); - } - - public void reset(int c1, int c2, int c3, int c4) { - stateChangedLatch = new CountDownLatch(c1); - stateEnteredLatch = new CountDownLatch(c2); - stateExitedLatch = new CountDownLatch(c3); - transitionLatch = new CountDownLatch(c4); - statesEntered.clear(); - statesExited.clear(); - transitionCount = 0; - transitions.clear(); - } - - } - - @SuppressWarnings("unchecked") - @BeforeEach - public void setup() { - context = new AnnotationConfigApplicationContext(); - context.register(CommonConfiguration.class, Application.class, Config.class); - context.refresh(); - machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); - listener = context.getBean(TestListener.class); - doStartAndAssert(machine); - } - - @AfterEach - public void clean() { - doStopAndAssert(machine); - context.close(); - context = null; - machine = null; - } - -} diff --git a/spring-statemachine-samples/zookeeper/spring-statemachine-samples-zookeeper.gradle b/spring-statemachine-samples/zookeeper/spring-statemachine-samples-zookeeper.gradle index f92feb903..f248b0179 100644 --- a/spring-statemachine-samples/zookeeper/spring-statemachine-samples-zookeeper.gradle +++ b/spring-statemachine-samples/zookeeper/spring-statemachine-samples-zookeeper.gradle @@ -9,5 +9,5 @@ dependencies { implementation project(':spring-statemachine-zookeeper') implementation project(':spring-statemachine-samples-common') implementation project(':spring-statemachine-core') - implementation 'org.springframework.shell:spring-shell-core' + implementation 'org.springframework.boot:spring-boot-starter' } diff --git a/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/Application.java b/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/Application.java index 09c3f096f..59cb00922 100644 --- a/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/Application.java +++ b/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/Application.java @@ -19,6 +19,7 @@ import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.config.EnableStateMachine; @@ -29,7 +30,7 @@ import org.springframework.statemachine.ensemble.StateMachineEnsemble; import org.springframework.statemachine.zookeeper.ZookeeperStateMachineEnsemble; -@Configuration +@SpringBootApplication(scanBasePackages = "demo") public class Application { @Configuration diff --git a/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/StateMachineCommands.java b/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/StateMachineCommands.java index 0583c6139..8665afc10 100644 --- a/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/StateMachineCommands.java +++ b/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/StateMachineCommands.java @@ -15,22 +15,30 @@ */ package demo.zookeeper; +import demo.BasicCommand; +import demo.Command; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.shell.command.annotation.Command; -import org.springframework.shell.command.annotation.Option; import demo.AbstractStateMachineCommands; import reactor.core.publisher.Mono; -@Command +@Configuration public class StateMachineCommands extends AbstractStateMachineCommands { - @Command(command = "sm event", description = "Sends an event to a state machine") - public String event(@Option(longNames = { "", "event" }, required = true, description = "The event") final String event) { - getStateMachine() - .sendEvent(Mono.just(MessageBuilder - .withPayload(event).build())) - .subscribe(); - return "Event " + event + " send"; + @Bean + public Command event() { + return new BasicCommand("event", "Sends an event to a state machine") { + @Override + public String execute(String[] args) { + String event = args[0]; + getStateMachine() + .sendEvent(Mono.just(MessageBuilder + .withPayload(event).build())) + .subscribe(); + return "Event " + event + " sent"; + } + }; } } diff --git a/spring-statemachine-samples/zookeeper/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/zookeeper/src/main/resources/META-INF/spring/spring-shell-plugin.xml deleted file mode 100644 index 1112bb354..000000000 --- a/spring-statemachine-samples/zookeeper/src/main/resources/META-INF/spring/spring-shell-plugin.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/spring-statemachine-samples/zookeeper/src/main/resources/application.properties b/spring-statemachine-samples/zookeeper/src/main/resources/application.properties new file mode 100644 index 000000000..709574239 --- /dev/null +++ b/spring-statemachine-samples/zookeeper/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/spring-statemachine-uml/spring-statemachine-uml.gradle b/spring-statemachine-uml/spring-statemachine-uml.gradle index da00a62b4..68f571a47 100644 --- a/spring-statemachine-uml/spring-statemachine-uml.gradle +++ b/spring-statemachine-uml/spring-statemachine-uml.gradle @@ -31,7 +31,7 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.springframework:spring-test' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' testImplementation 'org.awaitility:awaitility' } diff --git a/spring-statemachine-zookeeper/spring-statemachine-zookeeper.gradle b/spring-statemachine-zookeeper/spring-statemachine-zookeeper.gradle index 8a53368bb..804555a35 100644 --- a/spring-statemachine-zookeeper/spring-statemachine-zookeeper.gradle +++ b/spring-statemachine-zookeeper/spring-statemachine-zookeeper.gradle @@ -12,6 +12,6 @@ dependencies { testImplementation (project(':spring-statemachine-test')) testImplementation 'org.apache.curator:curator-test' testImplementation 'org.assertj:assertj-core' - testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'org.junit.platform:junit-platform-launcher' } diff --git a/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineEnsemble.java b/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineEnsemble.java index c3a6d6ea7..44a5171f9 100644 --- a/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineEnsemble.java +++ b/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachineEnsemble.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,9 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import com.esotericsoftware.kryo.Kryo; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.curator.framework.CuratorFramework; @@ -102,6 +104,25 @@ public ZookeeperStateMachineEnsemble(CuratorFramework curatorClient, String base * @param logSize the log size */ public ZookeeperStateMachineEnsemble(CuratorFramework curatorClient, String basePath, boolean cleanState, int logSize) { + this(curatorClient, basePath, cleanState, logSize, null); + } + + /** + * Instantiates a new zookeeper state machine ensemble with a custom Kryo + * configurer for application-specific allowlist registrations. + * + * @param curatorClient the curator client + * @param basePath the base zookeeper path + * @param cleanState if true clean existing state + * @param logSize the log size + * @param kryoCustomizer optional callback invoked once per Kryo instance after the + * framework's default registrations are applied. Use this to register + * application-specific state and event types (typically enums) so that they + * are accepted by the allowlist. May be {@code null}. + * @since 4.0.2 + */ + public ZookeeperStateMachineEnsemble(CuratorFramework curatorClient, String basePath, boolean cleanState, int logSize, + Consumer kryoCustomizer) { this.curatorClient = curatorClient; this.cleanState = cleanState; this.logSize = logSize; @@ -110,7 +131,7 @@ public ZookeeperStateMachineEnsemble(CuratorFramework curatorClient, String base this.logPath = baseDataPath + "/" + PATH_LOG; this.memberPath = basePath + "/" + PATH_MEMBERS; this.mutexPath = basePath + "/" + PATH_MUTEX; - this.persist = new ZookeeperStateMachinePersist(curatorClient, statePath, logPath, logSize); + this.persist = new ZookeeperStateMachinePersist(curatorClient, statePath, logPath, logSize, kryoCustomizer); setAutoStartup(true); } diff --git a/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachinePersist.java b/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachinePersist.java index b3d334474..3e6ed7c13 100644 --- a/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachinePersist.java +++ b/spring-statemachine-zookeeper/src/main/java/org/springframework/statemachine/zookeeper/ZookeeperStateMachinePersist.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2026 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,17 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Collection; -import java.util.UUID; +import java.util.function.Consumer; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.api.transaction.CuratorTransaction; import org.apache.curator.framework.api.transaction.CuratorTransactionFinal; import org.apache.curator.framework.api.transaction.CuratorTransactionResult; import org.apache.zookeeper.data.Stat; -import org.springframework.messaging.MessageHeaders; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.StateMachineException; import org.springframework.statemachine.StateMachinePersist; -import org.springframework.statemachine.kryo.MessageHeadersSerializer; -import org.springframework.statemachine.kryo.StateMachineContextSerializer; -import org.springframework.statemachine.kryo.UUIDSerializer; +import org.springframework.statemachine.kryo.KryoStateMachineSerialisationDefaults; import org.springframework.util.Assert; import com.esotericsoftware.kryo.Kryo; @@ -41,6 +38,14 @@ /** * {@link StateMachinePersist} using zookeeper as a storage and * kroy libraries as a backing serialization technique. + *

+ * Persisted bytes are deserialised through Kryo with the + * {@linkplain KryoStateMachineSerialisationDefaults safe-by-default class + * allowlist} applied. Applications that use custom state or event types + * (typically enums) must register those types via the + * {@link #ZookeeperStateMachinePersist(CuratorFramework, String, String, int, Consumer)} + * constructor's {@code kryoCustomizer} parameter; otherwise Kryo will reject + * them with {@code IllegalArgumentException} ("Class is not registered"). * * @author Janne Valkealahti * @@ -51,18 +56,7 @@ public class ZookeeperStateMachinePersist implements StateMachinePersist kryoThreadLocal = new ThreadLocal() { - - @SuppressWarnings("rawtypes") - @Override - protected Kryo initialValue() { - Kryo kryo = new Kryo(); - kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer()); - kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer()); - kryo.addDefaultSerializer(UUID.class, new UUIDSerializer()); - return kryo; - } - }; + private final ThreadLocal kryoThreadLocal; private final CuratorFramework curatorClient; private final String path; @@ -76,7 +70,7 @@ protected Kryo initialValue() { * @param path the path for persistent state */ public ZookeeperStateMachinePersist(CuratorFramework curatorClient, String path) { - this(curatorClient, path, null, 0); + this(curatorClient, path, null, 0, null); } /** @@ -88,6 +82,25 @@ public ZookeeperStateMachinePersist(CuratorFramework curatorClient, String path) * @param logSize the log size */ public ZookeeperStateMachinePersist(CuratorFramework curatorClient, String path, String logPath, int logSize) { + this(curatorClient, path, logPath, logSize, null); + } + + /** + * Instantiates a new zookeeper state machine persist with a custom Kryo + * configurer for application-specific allowlist registrations. + * + * @param curatorClient the curator client + * @param path the path + * @param logPath the log path (may be {@code null}) + * @param logSize the log size (must be a positive power of two when {@code logPath} is set) + * @param kryoCustomizer optional callback invoked once per Kryo instance after the + * framework's default registrations are applied. Use this to register + * application-specific state and event types (typically enums) so that they + * are accepted by the allowlist. May be {@code null}. + * @since 4.0.2 + */ + public ZookeeperStateMachinePersist(CuratorFramework curatorClient, String path, String logPath, int logSize, + Consumer kryoCustomizer) { if (logPath != null) { Assert.state(logSize > 0 && ((logSize & -logSize) == logSize), "Log size must be positive and power of two"); } @@ -95,6 +108,14 @@ public ZookeeperStateMachinePersist(CuratorFramework curatorClient, String path, this.path = path; this.logPath = logPath; this.logSize = logSize; + this.kryoThreadLocal = ThreadLocal.withInitial(() -> { + Kryo kryo = new Kryo(); + KryoStateMachineSerialisationDefaults.registerDefaults(kryo); + if (kryoCustomizer != null) { + kryoCustomizer.accept(kryo); + } + return kryo; + }); } @Override