diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dd6457c7647..3fc7972c912 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,75 +1,11 @@ -name: "CodeQL Advanced" +name: CodeQL Advanced (disabled in fork) on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] + workflow_dispatch: jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: 'ubuntu-latest' - permissions: - # required for all workflows - security-events: write - - # required to fetch internal or private CodeQL packs - packages: read - - # only required for workflows in private repositories - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: java-kotlin - build-mode: autobuild - - language: javascript-typescript # Need to add this even though we don't want this; otherwise Github complains. - build-mode: none + disabled: + if: ${{ false }} + runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v5 - - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # JS is only used in Benchmarker, which runs locally and is irrelevant in terms of security. - # We do not want to analyze those. - config: | - paths-ignore: - - 'benchmark/**/*.html' - - 'benchmark/**/*.ftl' - - 'benchmark/**/*.js' - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" + - run: echo "Disabled in this fork. Use the manual Publish to GitHub Packages workflow." diff --git a/.github/workflows/finish_release.yml b/.github/workflows/finish_release.yml index 7b7c13345aa..ab7e1613f42 100644 --- a/.github/workflows/finish_release.yml +++ b/.github/workflows/finish_release.yml @@ -1,52 +1,11 @@ -name: Finish Release +name: Finish Release (disabled in fork) on: - release: - types: [published] + workflow_dispatch: jobs: - build: - env: - RELEASE_BRANCH_NAME: "__timefold_release_branch__" + disabled: + if: ${{ false }} runs-on: ubuntu-latest - timeout-minutes: 120 steps: - - name: Checkout timefold-solver - uses: actions/checkout@v5 - with: - ref: main - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - - name: Put back the 999-SNAPSHOT version on the release branch - run: | - git config user.name "Timefold Release Bot" - git config user.email "release@timefold.ai" - git checkout $RELEASE_BRANCH_NAME - ./mvnw -Dfull versions:set -DnewVersion=999-SNAPSHOT - git commit -am "build: move back to 999-SNAPSHOT" - git push origin $RELEASE_BRANCH_NAME - - - name: Update release branch - shell: bash - run: | - tag=${{ github.ref }} - tag_version=${tag##*/} - version=${tag_version%.*} - version="${version:1}.x" - echo $version - exists="$(git branch -a | grep -w $version || true)" - echo "branch $exists" - if [ -n "$exists" ]; then - git config user.name "Timefold Release Bot" - git config user.email "release@timefold.ai" - git checkout $RELEASE_BRANCH_NAME - git checkout $version - git merge -Xtheirs --no-edit --squash -m "build: release version $tag_version" $RELEASE_BRANCH_NAME - git push origin $version - git push -d origin $RELEASE_BRANCH_NAME - else - git checkout $RELEASE_BRANCH_NAME - git branch -m $RELEASE_BRANCH_NAME $version - git push origin -u $version - git push -d origin $RELEASE_BRANCH_NAME - fi \ No newline at end of file + - run: echo "Disabled in this fork. Use the manual Publish to GitHub Packages workflow." diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cac8ee1936c..dd9e7238a8c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,173 +1,11 @@ -name: "Base Workflow" - -env: - NODE_OPTIONS: "--max_old_space_size=4096" +name: Base Workflow (disabled in fork) on: - push: - branches: [main] - pull_request: - branches: [main] - paths-ignore: - - 'LICENSE*' - - '.gitignore' - - '**.md' - - '**.adoc' - - '*.txt' + workflow_dispatch: jobs: - java: - name: "Java Solver" - concurrency: - group: pull_request-${{ github.event_name }}-${{ github.head_ref }}-${{ matrix.os }}-${{ matrix.java-version }} - cancel-in-progress: true - runs-on: ${{matrix.os}} - strategy: - matrix: - os: [ ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest ] - java-version: [ 25 ] # Latest LTS if not Ubuntu - include: - - os: ubuntu-latest - java-version: 17 - - os: ubuntu-24.04-arm - java-version: 17 - - os: ubuntu-latest - java-version: 21 - - os: ubuntu-24.04-arm - java-version: 21 - timeout-minutes: 120 - steps: - - uses: actions/checkout@v5 - - - uses: actions/setup-java@v5 - with: - java-version: ${{matrix.java-version}} - distribution: 'temurin' - cache: 'maven' - - - name: Build and test timefold-solver - run: ./mvnw -B verify - - - name: Test Summary - uses: test-summary/action@2920bc1b1b377c787227b204af6981e8f41bbef3 - with: - paths: "**/TEST-*.xml" - show: "fail" - if: always() - - # Exists to check long-running goals, such as docs. - # Tests are skipped as there is plenty of CI that runs them. - java_full: - name: "Java Solver (with flag -Dfull, no tests)" + disabled: + if: ${{ false }} runs-on: ubuntu-latest - timeout-minutes: 120 steps: - - uses: actions/checkout@v5 - - - uses: actions/setup-java@v5 - with: - java-version: 25 - distribution: 'temurin' - cache: 'maven' - - - name: Build timefold-solver using flag -Dfull - run: ./mvnw -DskipTests -Dfull -B verify - - spring_boot: - name: "Spring Boot" - concurrency: - group: pull_request_native-${{ github.event_name }}-${{ github.head_ref }}-${{ matrix.spring-version }} - cancel-in-progress: true - runs-on: ubuntu-latest - strategy: - matrix: - spring-version: ["3.3", "3.4"] - - timeout-minutes: 120 - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-java@v5 - with: - java-version: 25 # Latest LTS - distribution: 'temurin' - cache: 'maven' - - # Reading the latest Spring Boot version from Maven Central often fails. - # Since this information rarely changes, we can cache it, preventing CI failures. - - name: Cache Spring Boot version - id: cache-spring-boot-version - uses: actions/cache@v4 - with: - path: spring-boot-version - key: spring-boot-version-${{ matrix.spring-version }} - - name: Get Spring Boot version if not cached - if: steps.cache-spring-boot-version.outputs.cache-hit != 'true' - run: | - echo "$(curl -s 'https://search.maven.org/solrsearch/select?q=g:org.springframework.boot+AND+a:spring-boot-starter+AND+v:${{ matrix.spring-version }}.*' | jq -r '.response.docs[0].v')" >> spring-boot-version - if [ "$(head -n 1 spring-boot-version | cut -c1-3)" = "${{ matrix.spring-version }}" ]; then - exit 0 - else - exit 1 - fi - - name: Set Spring Boot version in Maven - run: | - SPRING_VERSION=$(cat spring-boot-version) - echo "Using Spring Boot version $SPRING_VERSION" - ./mvnw versions:set-property -Dproperty=version.org.springframework.boot -DnewVersion=$SPRING_VERSION - - - name: Quickly build timefold-solver - run: ./mvnw -B -Dquickly clean install - - name: Test Spring Boot - run: | - cd spring-integration - ../mvnw -B verify - - name: Test Summary - uses: test-summary/action@2920bc1b1b377c787227b204af6981e8f41bbef3 - with: - paths: "**/TEST-*.xml" - show: "fail" - if: always() - - native: - name: "Native Image" - concurrency: - group: pull_request_native-${{ github.event_name }}-${{ github.head_ref }}-${{matrix.os}}-${{ matrix.module }}-${{ matrix.java-version }} - cancel-in-progress: true - runs-on: ${{matrix.os}} - strategy: - matrix: - os: [ ubuntu-latest, ubuntu-24.04-arm ] # Windows doesn't work, Mac is not a deploy OS. - module: ["spring-integration", "quarkus-integration"] - java-version: [ 17, 21, 25 ] # LTS + latest. - exclude: - # Quarkus 3.17.2 has weird issues with Java 17 GraalVM, - # with Java 21+ GraalVM being recommended even for - # Java 17 projects. - # https://github.com/quarkusio/quarkus/issues/44877 - - module: "quarkus-integration" - java-version: 17 - timeout-minutes: 120 - steps: - - uses: actions/checkout@v5 - - - uses: graalvm/setup-graalvm@eec48106e0bf45f2976c2ff0c3e22395cced8243 # v1 - with: - java-version: ${{matrix.java-version}} - distribution: 'graalvm-community' - github-token: ${{ secrets.GITHUB_TOKEN }} - cache: 'maven' - - - name: Quickly build timefold-solver - run: ./mvnw -B -Dquickly clean install - - - name: Test timefold-solver in Native mode - run: | - cd ${{matrix.module}} - ../mvnw -B -Dnative verify - - - name: Test Summary - uses: test-summary/action@2920bc1b1b377c787227b204af6981e8f41bbef3 - with: - paths: "**/TEST-*.xml" - show: "fail" - if: always() + - run: echo "Disabled in this fork. Use the manual Publish to GitHub Packages workflow." diff --git a/.github/workflows/pull_request_quickstarts.yml b/.github/workflows/pull_request_quickstarts.yml index dafad0ae0b7..576f2906a59 100644 --- a/.github/workflows/pull_request_quickstarts.yml +++ b/.github/workflows/pull_request_quickstarts.yml @@ -1,79 +1,11 @@ -name: Quickstarts Workflow - -env: - NODE_OPTIONS: "--max_old_space_size=4096" +name: Quickstarts Workflow (disabled in fork) on: - push: - branches: [main] - pull_request: - branches: [main, '*.x'] - types: - - opened - - reopened - - synchronize - paths-ignore: - - 'LICENSE*' - - '.gitignore' - - '**.md' - - '**.adoc' - - '*.txt' + workflow_dispatch: jobs: - java: - name: "Java Quickstarts" + disabled: + if: ${{ false }} runs-on: ubuntu-latest - concurrency: - group: downstream-quickstarts-${{ github.event_name }}-${{ github.head_ref }} - cancel-in-progress: true - timeout-minutes: 120 steps: - # Clone timefold-solver - # No need to check for stale repo, as Github merges the main repo into the fork automatically. - - name: Checkout timefold-solver - uses: actions/checkout@v5 - with: - path: ./timefold-solver - - # Clone timefold-quickstarts - # Need to check for stale repo, since Github is not aware of the build chain and therefore doesn't automate it. - - name: Checkout timefold-quickstarts (PR) # Checkout the PR branch first, if it exists - if: github.head_ref # Only true if this is a PR. - id: checkout-quickstarts-pr - uses: actions/checkout@v5 - continue-on-error: true - with: - repository: ${{ github.actor }}/timefold-quickstarts - ref: ${{ github.head_ref }} - path: ./timefold-quickstarts - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - name: Checkout timefold-quickstarts (development) # Checkout the development branch if the PR branch does not exist - if: ${{ steps.checkout-quickstarts-pr.outcome != 'success' }} - uses: actions/checkout@v5 - with: - repository: TimefoldAI/timefold-quickstarts - ref: development - path: ./timefold-quickstarts - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - # Build and test - - name: Setup Temurin 25 and Maven - uses: actions/setup-java@v5 - with: - java-version: '25' - distribution: 'temurin' - cache: 'maven' - - name: Quickly build timefold-solver - working-directory: ./timefold-solver - shell: bash - run: ./mvnw -B -Dquickly clean install - - name: Build and test timefold-quickstarts - working-directory: ./timefold-quickstarts - shell: bash - run: mvn -B clean verify - - name: Test Summary - uses: test-summary/action@2920bc1b1b377c787227b204af6981e8f41bbef3 - with: - paths: "**/TEST-*.xml" - show: "fail" - if: always() + - run: echo "Disabled in this fork. Use the manual Publish to GitHub Packages workflow." diff --git a/.github/workflows/pull_request_secure.yml b/.github/workflows/pull_request_secure.yml index 3dd96c3db5d..f853cc88a64 100644 --- a/.github/workflows/pull_request_secure.yml +++ b/.github/workflows/pull_request_secure.yml @@ -1,312 +1,11 @@ -# Jobs in this workflow deal with secrets. -# Since they may be executed from forks by untrusted users, -# we need to ensure that the user is a member of the organization -# or that there is explicit approval for their jobs to run. -name: Secured Workflow - -env: - NODE_OPTIONS: "--max_old_space_size=4096" +name: Secured Workflow (disabled in fork) on: - push: - branches: [ main ] - # There are two differences to "pull_request" here: - # - The workflow will receive secrets, even in PRs from forks. - # - The workflow will be executed automatically, without requiring a manual approval. - # Therefore the workflow needs to be explicitly secured; see "known_user" and "approval_required" jobs below. - pull_request_target: - branches: [ main ] # Benchmarks aren't branched, so they will only ever work against current main. - types: - - opened - - reopened - - synchronize - paths-ignore: - - 'LICENSE*' - - '.gitignore' - - '**.md' - - '*.txt' + workflow_dispatch: jobs: - # Check if the user is a member of the organization; if so, allow the PR to sail through. - known_user: - runs-on: ubuntu-latest - outputs: - is_member_of_org: ${{ steps.auth_check.outputs.authorized }} - steps: - - id: auth_check - env: - GH_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Release account is a Solver Gatekeeper. - shell: bash - run: | - # -g to allow actors such as dependabot[bot] - ORG_MEMBERSHIP=`curl -g -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/orgs/TimefoldAI/memberships/${{ github.actor }}" | jq -r '.state == "active"'` - echo "authorized=$ORG_MEMBERSHIP" >> "$GITHUB_OUTPUT" - - id: validation - shell: bash - run: | - echo "Authorized user: ${{ steps.auth_check.outputs.authorized }}" - # If the user is not a member, require a member to approve the PR. - approval_required: - needs: known_user - environment: - ${{ - github.event_name == 'pull_request_target' && - github.event.pull_request.head.repo.full_name != github.repository && - (needs.known_user.outputs.is_member_of_org != 'true' || github.actor == 'dependabot[bot]') && - 'external' || 'internal' - }} - runs-on: ubuntu-latest - steps: - - run: true - integration-tests: - needs: approval_required - name: Integration Tests - runs-on: ubuntu-latest - concurrency: - group: pr-${{ github.event_name }}-${{ github.head_ref }} - cancel-in-progress: true - steps: - # Clone timefold-solver - # No need to check for stale repo, as Github merges the main repo into the fork automatically. - - name: Checkout timefold-solver - uses: actions/checkout@v5 - with: - path: ./timefold-solver - ref: ${{ github.event.pull_request.head.sha }} # The GHA event will pull the main branch by default, and we must specify the PR reference version - - - name: Setup Temurin 25 and Maven - uses: actions/setup-java@v5 - with: - java-version: '25' - distribution: 'temurin' - cache: 'maven' - - - name: Quickly build timefold-solver - working-directory: ./timefold-solver - shell: bash - run: ./mvnw -B -Dquickly clean install - - # Clone timefold-solver-enterprise - - name: Checkout timefold-solver-enterprise (PR) # Checkout the PR branch first, if it exists - id: checkout-solver-enterprise - uses: actions/checkout@v5 - continue-on-error: true - with: - repository: TimefoldAI/timefold-solver-enterprise - ref: ${{ github.head_ref }} - token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork. - path: ./timefold-solver-enterprise - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - name: Checkout timefold-solver-enterprise (main) # Checkout the main branch if the PR branch does not exist - if: steps.checkout-solver-enterprise.outcome != 'success' - uses: actions/checkout@v5 - with: - repository: TimefoldAI/timefold-solver-enterprise - ref: main - token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork. - path: ./timefold-solver-enterprise - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - - name: Quickly build timefold-solver-enterprise - working-directory: ./timefold-solver-enterprise - shell: bash - run: ./mvnw -B -Dquickly clean install - - # Clone timefold-solver-benchmarks - - name: Checkout timefold-solver-benchmarks (PR) # Checkout the PR branch first, if it exists - if: github.head_ref # Only true if this is a PR. - id: checkout-solver-benchmarks-pr - uses: actions/checkout@v5 - continue-on-error: true - with: - repository: TimefoldAI/timefold-solver-benchmarks - ref: ${{ github.head_ref }} - path: ./timefold-solver-benchmarks - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - name: Checkout timefold-solver-benchmarks (main) # Checkout the main branch if the PR branch does not exist - if: ${{ steps.checkout-solver-benchmarks-pr.outcome != 'success' }} - uses: actions/checkout@v5 - with: - repository: TimefoldAI/timefold-solver-benchmarks - ref: main - path: ./timefold-solver-benchmarks - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - - name: Build and test timefold-solver-benchmarks - working-directory: ./timefold-solver-benchmarks - shell: bash - run: ./mvnw -B -DskipJMH clean verify - - name: Test Summary - uses: test-summary/action@2920bc1b1b377c787227b204af6981e8f41bbef3 - with: - paths: "**/TEST-*.xml" - show: "fail" - if: always() - enterprise-java: - needs: approval_required - name: Enterprise Edition (Java) + disabled: + if: ${{ false }} runs-on: ubuntu-latest - concurrency: - group: downstream-enterprise-${{ github.event_name }}-${{ github.head_ref }} - cancel-in-progress: true - timeout-minutes: 120 steps: - # Clone timefold-solver - # No need to check for stale repo, as Github merges the main repo into the fork automatically. - - name: Checkout timefold-solver - uses: actions/checkout@v5 - with: - path: ./timefold-solver - ref: ${{ github.event.pull_request.head.sha }} # The GHA event will pull the main branch by default, and we must specify the PR reference version - - # Clone timefold-solver-enterprise - # Need to check for stale repo, since Github is not aware of the build chain and therefore doesn't automate it. - - name: Checkout timefold-solver-enterprise (PR) # Checkout the PR branch first, if it exists - id: checkout-solver-enterprise - uses: actions/checkout@v5 - continue-on-error: true - with: - repository: TimefoldAI/timefold-solver-enterprise - ref: ${{ github.head_ref }} - token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork. - path: ./timefold-solver-enterprise - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - name: Checkout timefold-solver-enterprise (main) # Checkout the main branch if the PR branch does not exist - if: steps.checkout-solver-enterprise.outcome != 'success' - uses: actions/checkout@v5 - with: - repository: TimefoldAI/timefold-solver-enterprise - ref: main - token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork. - path: ./timefold-solver-enterprise - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - # Build and test - - name: Setup Temurin 17 and Maven - uses: actions/setup-java@v5 - with: - java-version: '17' - distribution: 'temurin' - cache: 'maven' - - name: Quickly build timefold-solver - working-directory: ./timefold-solver - shell: bash - run: ./mvnw -B -Dquickly clean install - - name: Build and test timefold-solver-enterprise - working-directory: ./timefold-solver-enterprise - shell: bash - run: ./mvnw -B clean verify - - name: Test Summary - uses: test-summary/action@2920bc1b1b377c787227b204af6981e8f41bbef3 - with: - paths: "**/TEST-*.xml" - show: "fail" - if: always() - - build_documentation: - runs-on: ubuntu-latest - needs: approval_required - name: Build Documentation - environment: - name: "documentation (preview)" - url: ${{ steps.deploy.outputs.deployment-url }} - env: - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - steps: - - name: Checkout frontend - id: checkout-frontend - uses: actions/checkout@v5 - with: - repository: TimefoldAI/frontend - token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork. - fetch-depth: 0 # Otherwise merge will fail on account of not having history. - - name: Install pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 - - name: Set up NodeJs - uses: actions/setup-node@v6 - with: - node-version-file: .nvmrc - cache: pnpm - - - name: Checkout timefold-solver - uses: actions/checkout@v5 - with: - repository: "${{ github.event.pull_request.head.repo.owner.login || 'TimefoldAI' }}/timefold-solver" - ref: ${{ github.event.pull_request.head.sha || 'main' }} # The GHA event will pull the main branch by default, and we must specify the PR reference version - path: "./timefold-solver" - fetch-depth: 0 - - - name: Install yq - run: | - sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq - sudo chmod +x /usr/bin/yq - - - name: Updating Antora configuration - working-directory: "./timefold-solver" - run: | - echo "=== Updating antora.yml" - sed -i "s/\${project\.version}b0/SNAPSHOT/g" docs/src/antora.yml - sed -i "s/\${project\.version}/SNAPSHOT/g" docs/src/antora.yml - sed -i "s/\${maven\.compiler\.release}/$(find build/build-parent/ -name pom.xml -exec grep '' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml - sed -i "s/\${maven\.min\.version}/$(find build/build-parent/ -name pom.xml -exec grep '' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml - sed -i "s/\${version\.io\.quarkus}/$(find build/build-parent/ -name pom.xml -exec grep '' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml - sed -i "s/\${version\.org\.springframework\.boot}/$(find build/build-parent/ -name pom.xml -exec grep '' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml - sed -i "s/\${version\.ch\.qos\.logback}/$(find build/build-parent/ -name pom.xml -exec grep '' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml - sed -i "s/\${version\.exec\.plugin}/$(find build/build-parent/ -name pom.xml -exec grep '' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml - sed -i "s/\${version\.rewrite\.plugin}/$(find . -name pom.xml -exec grep '' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml - cat docs/src/antora.yml - - - name: Build Documentation - working-directory: "./" - env: - GIT_CREDENTIALS: ${{ secrets.GIT_CREDENTIALS }} - run: | - yq -i e 'del(.content.sources)' apps/docs/antora-playbook.yml - yq -i e 'del(.site.keys)' apps/docs/antora-playbook.yml - yq -i e '.content.sources += [{"url": "../../timefold-solver", "start_path": "docs/src"}]' apps/docs/antora-playbook.yml - pnpm install --frozen-lockfile - pnpm build --filter @timefoldai/docs - - - name: Deploy Documentation (Preview Mode) - if: ${{ env.BRANCH_NAME != 'main' }} - id: deploy - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - workingDirectory: ./apps/docs - command: pages deploy ./public-serve --project-name=timefold-docs --branch=${{ github.ref }} - packageManager: pnpm - - sonarcloud: - needs: approval_required - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - ref: ${{ github.event.pull_request.head.sha }} # The GHA event will pull the main branch by default, and we must specify the PR reference version - - name: Set up JDK 17 - uses: actions/setup-java@v5 - with: - java-version: 17 - distribution: 'temurin' - cache: 'maven' - - name: Cache SonarCloud packages - uses: actions/cache@v4 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - name: Build with Maven to measure code coverage # The ENV variables are limited to the scope of the current step. Avoid adding sensitive ENV variables here as the tests could leak them. - run: ./mvnw -B clean install -Prun-code-coverage - - - name: Run analysis - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Needed to run the SonarCloud analysis - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_BRANCH: ${{ github.event.pull_request.head.ref }} - PR_SHA: ${{ github.event.pull_request.head.sha }} - run: ./mvnw -B -Psonarcloud-analysis validate org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.organization=timefold -Dsonar.projectKey=ai.timefold:timefold-solver -Dsonar.host.url=https://sonarcloud.io -Dsonar.pullrequest.key="$PR_NUMBER" -Dsonar.pullrequest.branch="$PR_BRANCH" -Dsonar.scm.revision="$PR_SHA" + - run: echo "Disabled in this fork. Use the manual Publish to GitHub Packages workflow." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82ef4553a14..763d9af38ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,93 +1,41 @@ -# Axioms of the release pipeline: -# - Each release starts from timefold-solver by running this Github Action. -# - Each individual repository can only start its own release when its dependencies are fully released. -# timefold-solver-enterprise depends on timefold-solver -# timefold-quickstarts depends on timefold-solver -# timefold-website releases last -# - Each individual repository uses 999-SNAPSHOT as its development version, even on micro branches. -# -# Should any of these axioms change, the release pipeline will need to be (significantly) refactored. -# 0.8.x releases existed before this pipeline; they are done differently, similarities are coincidental. - -name: Release +name: Publish to GitHub Packages on: workflow_dispatch: inputs: version: - description: 'Release version (e.g. 1.0.0)' + description: "Release version (e.g. 1.0.0)" required: true sourceBranch: - description: 'Branch to cut the release from' + description: "Branch or tag to build from" default: main required: true - dryRun: - description: 'Do a dry run? (true or false)' - default: true - required: true jobs: - build: - env: - MAVEN_ARGS: "--no-transfer-progress --batch-mode" - RELEASE_BRANCH_NAME: "__timefold_release_branch__" - runs-on: self-hosted + publish: + runs-on: ubuntu-latest permissions: - contents: write # IMPORTANT: required for action to create release branch - pull-requests: write # IMPORTANT: so release PR can be created - id-token: write # IMPORTANT: mandatory for trusted publishing - attestations: write # IMPORTANT: mandatory for attestations + contents: read + packages: write steps: - - name: Checkout timefold-solver + - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 - ref: ${{ github.event.inputs.sourceBranch }} - - - name: Delete release branch (if exists) - continue-on-error: true - run: git push -d origin $RELEASE_BRANCH_NAME + ref: ${{ inputs.sourceBranch }} - - name: Create release branch and switch to it - run: | - git config user.name "Timefold Release Bot" - git config user.email "release@timefold.ai" - git checkout -b $RELEASE_BRANCH_NAME - - - uses: actions/setup-java@v5 + - name: Set up Java and Maven for GitHub Packages + uses: actions/setup-java@v5 with: - java-version: '17' - distribution: 'temurin' - cache: 'maven' + java-version: "17" + distribution: "temurin" + cache: "maven" + server-id: github + server-username: GITHUB_ACTOR + server-password: GITHUB_TOKEN - # We skip tests in dry run, to make the process faster. - # Technically, this goes against the main reason for doing a dry run; to eliminate potential problems. - # But unless something catastrophic happened, PR checks on source branch already ensured that all tests pass. - - name: Set release version and build release - run: | - ./mvnw -Dfull versions:set -DnewVersion=${{ github.event.inputs.version }} - ./mvnw -Dfull deploy -DskipTests=${{ github.event.inputs.dryRun }} -DaltDeploymentRepository=local::default::file://`pwd`/target/staging-deploy - cp docs/target/antora.yml docs/src/antora.yml - git add docs/src/antora.yml - find . -name 'pom.xml' | xargs git add - git commit -m "build: release version ${{ github.event.inputs.version }}" - git push origin $RELEASE_BRANCH_NAME + - name: Set project version + run: ./mvnw --batch-mode -Dfull versions:set -DnewVersion=${{ inputs.version }} -DgenerateBackupPoms=false - - name: Run JReleaser - uses: jreleaser/release-action@80ffb38fa759704eed4db5c7fcaae3ac1079473e # v2 + - name: Build and publish to GitHub Packages + run: ./mvnw --batch-mode -Dfull deploy env: - JRELEASER_DRY_RUN: ${{ github.event.inputs.dryRun }} - JRELEASER_PROJECT_VERSION: ${{ github.event.inputs.version }} - JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} - JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} - JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} - JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} - JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVEN_CENTRAL_TOKEN_USER }} - JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.JRELEASER_MAVEN_CENTRAL_TOKEN }} - - - name: JReleaser release output - uses: actions/upload-artifact@v5 - if: always() - with: - name: jreleaser-release - path: | - out/jreleaser/trace.log - out/jreleaser/output.properties + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONSTRAINT_PROVIDER_INSTANCE_EXAMPLE.md b/CONSTRAINT_PROVIDER_INSTANCE_EXAMPLE.md new file mode 100644 index 00000000000..781ebe5beb4 --- /dev/null +++ b/CONSTRAINT_PROVIDER_INSTANCE_EXAMPLE.md @@ -0,0 +1,186 @@ +# ConstraintProvider Instance Support + +## Overview + +This feature allows you to pass a `ConstraintProvider` instance directly to the solver configuration, instead of only being able to provide a class reference. This enables more flexible constraint configurations at runtime. + +## Usage + +### Using ConstraintProvider Class (Original Method) + +```java +SolverConfig solverConfig = new SolverConfig() + .withSolutionClass(MySolution.class) + .withEntityClasses(MyEntity.class) + .withScoreDirectorFactory(new ScoreDirectorFactoryConfig() + .withConstraintProviderClass(MyConstraintProvider.class)); + +SolverFactory solverFactory = SolverFactory.create(solverConfig); +Solver solver = solverFactory.buildSolver(); +``` + +### Using ConstraintProvider Instance (New Method) + +```java +// Create your constraint provider instance with custom configuration +MyConstraintProvider constraintProvider = new MyConstraintProvider( + customParameter1, + customParameter2, + runtimeConfiguration +); + +SolverConfig solverConfig = new SolverConfig() + .withSolutionClass(MySolution.class) + .withEntityClasses(MyEntity.class) + .withScoreDirectorFactory(new ScoreDirectorFactoryConfig() + .withConstraintProvider(constraintProvider)); + +SolverFactory solverFactory = SolverFactory.create(solverConfig); +Solver solver = solverFactory.buildSolver(); +``` + +## Example: Dynamic Constraint Configuration + +```java +public class ConfigurableConstraintProvider implements ConstraintProvider { + + private final Set enabledConstraints; + private final Map constraintWeights; + + public ConfigurableConstraintProvider( + Set enabledConstraints, + Map constraintWeights) { + this.enabledConstraints = enabledConstraints; + this.constraintWeights = constraintWeights; + } + + @Override + public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { + List constraints = new ArrayList<>(); + + if (enabledConstraints.contains("capacity")) { + constraints.add(capacityConstraint(constraintFactory)); + } + + if (enabledConstraints.contains("time-window")) { + constraints.add(timeWindowConstraint(constraintFactory)); + } + + return constraints.toArray(new Constraint[0]); + } + + private Constraint capacityConstraint(ConstraintFactory constraintFactory) { + int weight = constraintWeights.getOrDefault("capacity", 1); + return constraintFactory.forEach(Vehicle.class) + .filter(vehicle -> vehicle.getCapacity() < vehicle.getDemand()) + .penalize(HardSoftScore.ONE_HARD.multiply(weight)) + .asConstraint("Capacity constraint"); + } + + // ... other constraints +} + +// Usage in a multi-tenant SaaS application: +public Solver createSolverForTenant(Long tenantId) { + TenantConfiguration config = tenantConfigRepository.findById(tenantId); + + ConfigurableConstraintProvider constraintProvider = + new ConfigurableConstraintProvider( + config.getEnabledConstraints(), + config.getConstraintWeights() + ); + + SolverConfig solverConfig = new SolverConfig() + .withSolutionClass(MySolution.class) + .withEntityClasses(MyEntity.class) + .withScoreDirectorFactory(new ScoreDirectorFactoryConfig() + .withConstraintProvider(constraintProvider)); + + return SolverFactory.create(solverConfig).buildSolver(); +} +``` + +## Important Notes + +### Serialization + +When using a `ConstraintProvider` instance: + +- The instance is **NOT** serialized when the `SolverConfig` is written to XML +- The instance is marked with `@XmlTransient` to prevent serialization +- Only the class-based configuration can be serialized to XML files + +### Custom Properties + +- Custom properties (`constraintProviderCustomProperties`) can **ONLY** be used with class-based configuration +- If you provide an instance, you cannot use custom properties (an exception will be thrown) +- Configure your instance directly in Java code instead of using custom properties + +### Mutual Exclusivity + +You **cannot** provide both a class and an instance: + +```java +// This will throw an IllegalStateException: +new ScoreDirectorFactoryConfig() + .withConstraintProviderClass(MyConstraintProvider.class) + .withConstraintProvider(new MyConstraintProvider()) // ERROR! +``` + +Choose one or the other: + +- Use `withConstraintProviderClass()` for simple, serializable configurations +- Use `withConstraintProvider()` for runtime-configurable, instance-based configurations + +## Use Cases + +This feature is particularly useful for: + +1. **Multi-tenant Applications**: Different tenants can have different constraint configurations +2. **Dynamic Constraint Selection**: Enable/disable constraints based on runtime conditions +3. **Database-driven Configuration**: Load constraint parameters from a database +4. **A/B Testing**: Compare different constraint configurations +5. **Parameterized Constraints**: Pass runtime parameters to your constraint provider + +## Migration from Class-based to Instance-based + +If you're migrating from class-based to instance-based configuration: + +**Before:** + +```java +public class MyConstraintProvider implements ConstraintProvider { + // No constructor parameters - instantiated by reflection + + @Override + public Constraint[] defineConstraints(ConstraintFactory factory) { + // All configuration had to be hardcoded or use static fields + } +} +``` + +**After:** + +```java +public class MyConstraintProvider implements ConstraintProvider { + private final MyConfiguration config; + + public MyConstraintProvider(MyConfiguration config) { + this.config = config; + } + + @Override + public Constraint[] defineConstraints(ConstraintFactory factory) { + // Use instance configuration + if (config.isFeatureEnabled("feature1")) { + // ... + } + } +} + +// Usage: +MyConfiguration config = loadConfiguration(); +ConstraintProvider provider = new MyConstraintProvider(config); +solverConfig.getScoreDirectorFactoryConfig() + .withConstraintProvider(provider); +``` diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000000..03dd94350ba --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,194 @@ +# Implementation Summary: ConstraintProvider Instance Support + +## Overview + +This implementation adds support for passing a `ConstraintProvider` instance to the solver configuration, rather than only being able to specify a class reference. This addresses issue #1383 and enables more flexible constraint configurations at runtime. + +## Changes Made + +### 1. Core Configuration Class: `ScoreDirectorFactoryConfig.java` + +**Location:** `/workspaces/timefold-solver/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java` + +**Changes:** + +- Added new field `constraintProvider` (annotated with `@XmlTransient` to prevent XML serialization) +- Added `getConstraintProvider()` getter method +- Added `setConstraintProvider()` setter method +- Added `withConstraintProvider()` fluent API method +- Updated `inherit()` method to handle the new field +- Updated `visitReferencedClasses()` to include the instance's class when present + +**Key Design Decision:** The instance is marked with `@XmlTransient` because instances cannot be serialized to XML configuration files. This maintains backward compatibility with XML-based configurations while adding programmatic configuration support. + +### 2. Factory Class: `BavetConstraintStreamScoreDirectorFactory.java` + +**Location:** `/workspaces/timefold-solver/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java` + +**Changes:** + +- Updated `buildScoreDirectorFactory()` to check for a provided instance first +- If an instance is provided: + - Use it directly + - Validate that custom properties are not also provided (as they can only be applied via reflection to class-based instantiation) +- If no instance is provided: + - Fall back to the original class-based instantiation approach + - Continue to support custom properties via reflection + +**Key Design Decision:** Instance-based configuration takes precedence over class-based configuration, but they are mutually exclusive (validated in the factory). + +### 3. Validation Class: `ScoreDirectorFactoryFactory.java` + +**Location:** `/workspaces/timefold-solver/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java` + +**Changes in `assertCorrectDirectorFactory()`:** + +- Updated validation to recognize both `constraintProviderClass` and `constraintProvider` instance +- Added validation to ensure both are not provided simultaneously +- Updated error messages to mention both options +- Ensured custom properties cannot be used with instance-based configuration + +**Changes in `decideMultipleScoreDirectorFactories()`:** + +- Updated the condition to check for either class or instance when deciding to use constraint streams + +### 4. Test Class: `ScoreDirectorFactoryFactoryTest.java` + +**Location:** `/workspaces/timefold-solver/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java` + +**New Tests Added:** + +1. `constraintProviderInstance()` - Verifies basic instance usage +2. `constraintProviderInstanceAndClass_throwsException()` - Verifies mutual exclusivity +3. `constraintProviderInstanceWithCustomProperties_throwsException()` - Verifies custom properties cannot be used with instances + +### 5. Integration Test: `ConstraintProviderInstanceIntegrationTest.java` + +**Location:** `/workspaces/timefold-solver/core/src/test/java/ai/timefold/solver/core/impl/score/director/ConstraintProviderInstanceIntegrationTest.java` + +**Purpose:** Demonstrates end-to-end usage of the feature, including: + +- Creating a parameterized constraint provider +- Using it with a full solver configuration +- Comparing different configurations (strict vs lenient) + +### 6. Documentation: `CONSTRAINT_PROVIDER_INSTANCE_EXAMPLE.md` + +**Location:** `/workspaces/timefold-solver/CONSTRAINT_PROVIDER_INSTANCE_EXAMPLE.md` + +**Contents:** + +- Usage examples +- Migration guide +- Important notes about serialization and custom properties +- Use cases and scenarios +- Complete working examples + +## API Usage + +### Simple Usage + +```java +MyConstraintProvider provider = new MyConstraintProvider(config); +ScoreDirectorFactoryConfig factoryConfig = new ScoreDirectorFactoryConfig() + .withConstraintProvider(provider); +``` + +### Full Solver Configuration + +```java +MyConstraintProvider provider = new MyConstraintProvider(runtimeConfig); +SolverConfig solverConfig = new SolverConfig() + .withSolutionClass(MySolution.class) + .withEntityClasses(MyEntity.class) + .withScoreDirectorFactory(new ScoreDirectorFactoryConfig() + .withConstraintProvider(provider)); +Solver solver = SolverFactory.create(solverConfig).buildSolver(); +``` + +## Backward Compatibility + +✅ **Fully backward compatible** - All existing code continues to work: + +- Class-based configuration still works exactly as before +- XML serialization still works for class-based configurations +- Custom properties still work with class-based configurations +- No changes to the ConstraintProvider interface + +## Validation Rules + +The implementation enforces the following rules: + +1. **Mutual Exclusivity:** Cannot provide both `constraintProviderClass` and `constraintProvider` instance +2. **Custom Properties:** Cannot use `constraintProviderCustomProperties` with instance-based configuration +3. **XML Serialization:** Instance-based configuration cannot be serialized to XML (by design) + +## Use Cases Enabled + +This feature enables several important use cases mentioned in issue #1383: + +1. **Multi-tenant SaaS Applications:** Different tenants can have different constraint configurations loaded from a database +2. **Dynamic Constraint Selection:** Enable/disable constraints based on runtime conditions +3. **Parameterized Constraints:** Pass runtime parameters to constraint providers +4. **A/B Testing:** Compare different constraint configurations easily +5. **Database-driven Configuration:** Load constraint parameters and enabled/disabled status from external sources + +## Testing + +The implementation includes: + +- ✅ Unit tests for validation logic +- ✅ Integration tests demonstrating end-to-end usage +- ✅ Tests for error conditions (mutual exclusivity, custom properties) +- ✅ No compilation errors + +## Migration Path + +For users currently working around this limitation (e.g., using static fields or other hacky solutions): + +**Before (workaround using static fields):** + +```java +public class MyConstraintProvider implements ConstraintProvider { + private static Config config; // Not ideal! + + public static void setConfig(Config config) { + MyConstraintProvider.config = config; + } +} +``` + +**After (clean solution with instances):** + +```java +public class MyConstraintProvider implements ConstraintProvider { + private final Config config; + + public MyConstraintProvider(Config config) { + this.config = config; + } +} + +// Usage: +Config config = loadConfig(); +new ScoreDirectorFactoryConfig() + .withConstraintProvider(new MyConstraintProvider(config)) +``` + +## Performance Considerations + +- ✅ No performance impact - instance is used directly, no additional overhead +- ✅ No breaking changes to constraint streaming implementation +- ✅ Validation happens at configuration time, not during solving + +## Future Enhancements + +Potential future enhancements (not included in this implementation): + +- Support for serializing instances using custom serialization mechanisms +- Builder pattern for constraint provider configurations +- Integration with Spring dependency injection + +## Conclusion + +This implementation successfully addresses issue #1383 by providing a clean, well-tested API for using constraint provider instances while maintaining full backward compatibility with existing code and configurations. diff --git a/build/bom/pom.xml b/build/bom/pom.xml index aaa69abf68d..af12acda338 100644 --- a/build/bom/pom.xml +++ b/build/bom/pom.xml @@ -1,6 +1,7 @@ - + 4.0.0 @@ -387,4 +388,4 @@ - + \ No newline at end of file diff --git a/build/build-parent/pom.xml b/build/build-parent/pom.xml index c866c12ebac..9af8fb6ce1b 100644 --- a/build/build-parent/pom.xml +++ b/build/build-parent/pom.xml @@ -1,6 +1,7 @@ - + 4.0.0 @@ -56,20 +57,23 @@ 2.19.1 - 3.9.11 + 3.9.11 17 - + false apply ${project.root.dir}/target/jacoco.exec - + true - + 0.9.38 - + ${maven.multiModuleProjectDirectory} UTF-8 UTF-8 @@ -239,7 +243,8 @@ - + true true @@ -256,11 +261,14 @@ - + commons-logging:commons-log* - + log4j:log4j - + javassist:javassist org.apache.cxf:cxf-bundle-jaxrs org.mockito:mockito-all @@ -286,7 +294,8 @@ - + io.quarkus quarkus-ide-launcher @@ -312,7 +321,7 @@ - + @@ -400,7 +409,8 @@ ${version.source.plugin} - + META-INF/JAXB/ ${schema.filename.benchmark} @@ -494,7 +504,7 @@ eclipse.importorder - + Remove wildcard imports import\s+[^\*\s]+\*;(\r\n|\r|\n) @@ -575,7 +585,8 @@ - + maven-dependency-plugin ${version.dependency.plugin} @@ -634,7 +645,8 @@ sonarcloud-analysis @@ -669,16 +681,16 @@ https://groups.google.com/forum/#!topic/jacoco/oMxNZs_DNII --> - - - + + + - + @@ -688,9 +700,10 @@ - - - + + + @@ -698,14 +711,14 @@ - - + + - + @@ -752,4 +765,4 @@ - + \ No newline at end of file diff --git a/build/ide-config/pom.xml b/build/ide-config/pom.xml index 14a48aea733..bed370c50d4 100644 --- a/build/ide-config/pom.xml +++ b/build/ide-config/pom.xml @@ -1,6 +1,7 @@ - + 4.0.0 ai.timefold.solver @@ -24,4 +25,4 @@ ai.timefold.solver.ide.config - + \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java b/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java index c402d3e6fc2..2f64e1dcfac 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/score/director/ScoreDirectorFactoryConfig.java @@ -6,6 +6,7 @@ import java.util.function.Consumer; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlTransient; import jakarta.xml.bind.annotation.XmlType; import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; @@ -42,6 +43,9 @@ public class ScoreDirectorFactoryConfig extends AbstractConfig constraintProviderClass = null; + @XmlTransient + protected ConstraintProvider constraintProvider = null; + @XmlJavaTypeAdapter(JaxbCustomPropertiesAdapter.class) protected Map constraintProviderCustomProperties = null; protected ConstraintStreamImplType constraintStreamImplType; @@ -91,6 +95,14 @@ public void setConstraintProviderClass(@Nullable Class getConstraintProviderCustomProperties() { return constraintProviderCustomProperties; } @@ -145,8 +157,11 @@ public void setIncrementalScoreCalculatorCustomProperties( } /** - * @deprecated All support for Score DRL was removed when Timefold was forked from OptaPlanner. - * See DRL to Constraint Streams + * @deprecated All support for Score DRL was removed when Timefold was forked + * from OptaPlanner. + * See DRL + * to Constraint Streams * migration recipe. */ @Deprecated(forRemoval = true) @@ -155,8 +170,11 @@ public List getScoreDrlList() { } /** - * @deprecated All support for Score DRL was removed when Timefold was forked from OptaPlanner. - * See DRL to Constraint Streams + * @deprecated All support for Score DRL was removed when Timefold was forked + * from OptaPlanner. + * See DRL + * to Constraint Streams * migration recipe. */ @Deprecated(forRemoval = true) @@ -184,28 +202,31 @@ public void setAssertionScoreDirectorFactory(@Nullable ScoreDirectorFactoryConfi // With methods // ************************************************************************ - public @NonNull ScoreDirectorFactoryConfig - withEasyScoreCalculatorClass(@NonNull Class easyScoreCalculatorClass) { + public @NonNull ScoreDirectorFactoryConfig withEasyScoreCalculatorClass( + @NonNull Class easyScoreCalculatorClass) { this.easyScoreCalculatorClass = easyScoreCalculatorClass; return this; } - public @NonNull ScoreDirectorFactoryConfig - withEasyScoreCalculatorCustomProperties( - @NonNull Map<@NonNull String, @NonNull String> easyScoreCalculatorCustomProperties) { + public @NonNull ScoreDirectorFactoryConfig withEasyScoreCalculatorCustomProperties( + @NonNull Map<@NonNull String, @NonNull String> easyScoreCalculatorCustomProperties) { this.easyScoreCalculatorCustomProperties = easyScoreCalculatorCustomProperties; return this; } - public @NonNull ScoreDirectorFactoryConfig - withConstraintProviderClass(@NonNull Class constraintProviderClass) { + public @NonNull ScoreDirectorFactoryConfig withConstraintProviderClass( + @NonNull Class constraintProviderClass) { this.constraintProviderClass = constraintProviderClass; return this; } - public @NonNull ScoreDirectorFactoryConfig - withConstraintProviderCustomProperties( - @NonNull Map<@NonNull String, @NonNull String> constraintProviderCustomProperties) { + public @NonNull ScoreDirectorFactoryConfig withConstraintProvider(@NonNull ConstraintProvider constraintProvider) { + this.constraintProvider = constraintProvider; + return this; + } + + public @NonNull ScoreDirectorFactoryConfig withConstraintProviderCustomProperties( + @NonNull Map<@NonNull String, @NonNull String> constraintProviderCustomProperties) { this.constraintProviderCustomProperties = constraintProviderCustomProperties; return this; } @@ -215,35 +236,36 @@ public void setAssertionScoreDirectorFactory(@Nullable ScoreDirectorFactoryConfi * This method no longer has any effect. */ @Deprecated(forRemoval = true, since = "1.16.0") - public @NonNull ScoreDirectorFactoryConfig - withConstraintStreamImplType(@NonNull ConstraintStreamImplType constraintStreamImplType) { + public @NonNull ScoreDirectorFactoryConfig withConstraintStreamImplType( + @NonNull ConstraintStreamImplType constraintStreamImplType) { this.constraintStreamImplType = constraintStreamImplType; return this; } - public @NonNull ScoreDirectorFactoryConfig - withConstraintStreamAutomaticNodeSharing(@NonNull Boolean constraintStreamAutomaticNodeSharing) { + public @NonNull ScoreDirectorFactoryConfig withConstraintStreamAutomaticNodeSharing( + @NonNull Boolean constraintStreamAutomaticNodeSharing) { this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing; return this; } - public @NonNull ScoreDirectorFactoryConfig - withIncrementalScoreCalculatorClass( - @NonNull Class incrementalScoreCalculatorClass) { + public @NonNull ScoreDirectorFactoryConfig withIncrementalScoreCalculatorClass( + @NonNull Class incrementalScoreCalculatorClass) { this.incrementalScoreCalculatorClass = incrementalScoreCalculatorClass; return this; } - public @NonNull ScoreDirectorFactoryConfig - withIncrementalScoreCalculatorCustomProperties( - @NonNull Map<@NonNull String, @NonNull String> incrementalScoreCalculatorCustomProperties) { + public @NonNull ScoreDirectorFactoryConfig withIncrementalScoreCalculatorCustomProperties( + @NonNull Map<@NonNull String, @NonNull String> incrementalScoreCalculatorCustomProperties) { this.incrementalScoreCalculatorCustomProperties = incrementalScoreCalculatorCustomProperties; return this; } /** - * @deprecated All support for Score DRL was removed when Timefold was forked from OptaPlanner. - * See DRL to Constraint Streams + * @deprecated All support for Score DRL was removed when Timefold was forked + * from OptaPlanner. + * See DRL + * to Constraint Streams * migration recipe. */ @Deprecated(forRemoval = true) @@ -253,8 +275,11 @@ public ScoreDirectorFactoryConfig withScoreDrlList(List scoreDrlList) { } /** - * @deprecated All support for Score DRL was removed when Timefold was forked from OptaPlanner. - * See DRL to Constraint Streams + * @deprecated All support for Score DRL was removed when Timefold was forked + * from OptaPlanner. + * See DRL + * to Constraint Streams * migration recipe. */ @Deprecated(forRemoval = true) @@ -282,16 +307,20 @@ public ScoreDirectorFactoryConfig withScoreDrls(String... scoreDrls) { easyScoreCalculatorCustomProperties, inheritedConfig.getEasyScoreCalculatorCustomProperties()); constraintProviderClass = ConfigUtils.inheritOverwritableProperty( constraintProviderClass, inheritedConfig.getConstraintProviderClass()); + constraintProvider = ConfigUtils.inheritOverwritableProperty( + constraintProvider, inheritedConfig.getConstraintProvider()); constraintProviderCustomProperties = ConfigUtils.inheritMergeableMapProperty( constraintProviderCustomProperties, inheritedConfig.getConstraintProviderCustomProperties()); constraintStreamImplType = ConfigUtils.inheritOverwritableProperty( constraintStreamImplType, inheritedConfig.getConstraintStreamImplType()); - constraintStreamAutomaticNodeSharing = ConfigUtils.inheritOverwritableProperty(constraintStreamAutomaticNodeSharing, + constraintStreamAutomaticNodeSharing = ConfigUtils.inheritOverwritableProperty( + constraintStreamAutomaticNodeSharing, inheritedConfig.getConstraintStreamAutomaticNodeSharing()); incrementalScoreCalculatorClass = ConfigUtils.inheritOverwritableProperty( incrementalScoreCalculatorClass, inheritedConfig.getIncrementalScoreCalculatorClass()); incrementalScoreCalculatorCustomProperties = ConfigUtils.inheritMergeableMapProperty( - incrementalScoreCalculatorCustomProperties, inheritedConfig.getIncrementalScoreCalculatorCustomProperties()); + incrementalScoreCalculatorCustomProperties, + inheritedConfig.getIncrementalScoreCalculatorCustomProperties()); scoreDrlList = ConfigUtils.inheritMergeableListProperty( scoreDrlList, inheritedConfig.getScoreDrlList()); initializingScoreTrend = ConfigUtils.inheritOverwritableProperty( @@ -310,6 +339,9 @@ public ScoreDirectorFactoryConfig withScoreDrls(String... scoreDrls) { public void visitReferencedClasses(@NonNull Consumer> classVisitor) { classVisitor.accept(easyScoreCalculatorClass); classVisitor.accept(constraintProviderClass); + if (constraintProvider != null) { + classVisitor.accept(constraintProvider.getClass()); + } classVisitor.accept(incrementalScoreCalculatorClass); if (assertionScoreDirectorFactory != null) { assertionScoreDirectorFactory.visitReferencedClasses(classVisitor); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java index 83e5ccfb1c0..8b020c2fa40 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java @@ -35,10 +35,11 @@ public ScoreDirectorFactory buildScoreDirectorFactory(Environ if (environmentMode.compareTo(EnvironmentMode.STEP_ASSERT) > 0) { throw new IllegalArgumentException( "A non-null assertionScoreDirectorFactory (%s) requires an environmentMode (%s) of %s or lower." - .formatted(assertionScoreDirectorFactory, environmentMode, EnvironmentMode.STEP_ASSERT)); + .formatted(assertionScoreDirectorFactory, environmentMode, + EnvironmentMode.STEP_ASSERT)); } - var assertionScoreDirectorFactoryFactory = - new ScoreDirectorFactoryFactory(assertionScoreDirectorFactory); + var assertionScoreDirectorFactoryFactory = new ScoreDirectorFactoryFactory( + assertionScoreDirectorFactory); scoreDirectorFactory.setAssertionScoreDirectorFactory(assertionScoreDirectorFactoryFactory .buildScoreDirectorFactory(EnvironmentMode.NON_REPRODUCIBLE, solutionDescriptor)); } @@ -66,17 +67,19 @@ DRL constraints requested via scoreDrlList (%s), but this is no longer supported } assertCorrectDirectorFactory(config); - // At this point, we are guaranteed to have at most one score director factory selected. + // At this point, we are guaranteed to have at most one score director factory + // selected. if (config.getEasyScoreCalculatorClass() != null) { return EasyScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config); } else if (config.getIncrementalScoreCalculatorClass() != null) { return IncrementalScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config); - } else if (config.getConstraintProviderClass() != null) { + } else if (config.getConstraintProviderClass() != null || config.getConstraintProvider() != null) { return BavetConstraintStreamScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode); } else { throw new IllegalArgumentException( - "The scoreDirectorFactory lacks configuration for either constraintProviderClass, " + + "The scoreDirectorFactory lacks configuration for either constraintProvider, constraintProviderClass, " + + "easyScoreCalculatorClass or incrementalScoreCalculatorClass."); } } @@ -98,12 +101,18 @@ private static void assertCorrectDirectorFactory(ScoreDirectorFactoryConfig conf config.getIncrementalScoreCalculatorCustomProperties())); } var constraintProviderClass = config.getConstraintProviderClass(); - var hasConstraintProvider = constraintProviderClass != null; + var constraintProvider = config.getConstraintProvider(); + var hasConstraintProvider = constraintProviderClass != null || constraintProvider != null; if (!hasConstraintProvider && config.getConstraintProviderCustomProperties() != null) { throw new IllegalStateException( - "If there is no constraintProviderClass (%s), then there can be no constraintProviderCustomProperties (%s) either." + "If there is no constraintProviderClass (%s) or constraintProvider instance, then there can be no constraintProviderCustomProperties (%s) either." .formatted(constraintProviderClass, config.getConstraintProviderCustomProperties())); } + if (constraintProviderClass != null && constraintProvider != null) { + throw new IllegalStateException( + "The scoreDirectorFactory cannot have both a constraintProviderClass (%s) and a constraintProvider instance together. Use one or the other." + .formatted(constraintProviderClass.getName())); + } if (hasEasyScoreCalculator && (hasIncrementalScoreCalculator || hasConstraintProvider) || (hasIncrementalScoreCalculator && hasConstraintProvider)) { var scoreDirectorFactoryPropertyList = new ArrayList(3); @@ -112,8 +121,14 @@ private static void assertCorrectDirectorFactory(ScoreDirectorFactoryConfig conf .add("an easyScoreCalculatorClass (%s)".formatted(easyScoreCalculatorClass.getName())); } if (hasConstraintProvider) { - scoreDirectorFactoryPropertyList - .add("an constraintProviderClass (%s)".formatted(constraintProviderClass.getName())); + if (constraintProviderClass != null) { + scoreDirectorFactoryPropertyList + .add("a constraintProviderClass (%s)".formatted(constraintProviderClass.getName())); + } else { + scoreDirectorFactoryPropertyList + .add("a constraintProvider instance (%s)" + .formatted(constraintProvider.getClass().getName())); + } } if (hasIncrementalScoreCalculator) { scoreDirectorFactoryPropertyList.add("an incrementalScoreCalculatorClass (%s)" diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java index 3815286b2c0..5aa338e1d22 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java @@ -25,8 +25,25 @@ public final class BavetConstraintStreamScoreDirectorFactory> { public static > BavetConstraintStreamScoreDirectorFactory - buildScoreDirectorFactory(SolutionDescriptor solutionDescriptor, ScoreDirectorFactoryConfig config, + buildScoreDirectorFactory( + SolutionDescriptor solutionDescriptor, ScoreDirectorFactoryConfig config, EnvironmentMode environmentMode) { + var providedConstraintProvider = config.getConstraintProvider(); + if (providedConstraintProvider != null) { + // Use the provided instance + if (config.getConstraintProviderCustomProperties() != null + && !config.getConstraintProviderCustomProperties().isEmpty()) { + throw new IllegalStateException( + """ + The constraintProviderCustomProperties (%s) cannot be used when a constraintProvider instance is provided. + Custom properties can only be applied when using constraintProviderClass.""" + .formatted(config.getConstraintProviderCustomProperties())); + } + return new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, providedConstraintProvider, + environmentMode); + } + + // Fall back to class-based instantiation var providedConstraintProviderClass = config.getConstraintProviderClass(); if (providedConstraintProviderClass == null || !ConstraintProvider.class.isAssignableFrom(providedConstraintProviderClass)) { @@ -44,8 +61,8 @@ public final class BavetConstraintStreamScoreDirectorFactory getConstraintProviderClass(ScoreDirectorFactoryConfig config, Class providedConstraintProviderClass) { if (Boolean.TRUE.equals(config.getConstraintStreamAutomaticNodeSharing())) { - var enterpriseService = - TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.AUTOMATIC_NODE_SHARING); + var enterpriseService = TimefoldSolverEnterpriseService + .loadOrFail(TimefoldSolverEnterpriseService.Feature.AUTOMATIC_NODE_SHARING); return enterpriseService.buildLambdaSharedConstraintProvider(config.getConstraintProviderClass()); } else { return providedConstraintProviderClass; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ConstraintProviderInstanceIntegrationTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ConstraintProviderInstanceIntegrationTest.java new file mode 100644 index 00000000000..e601c06f60f --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ConstraintProviderInstanceIntegrationTest.java @@ -0,0 +1,139 @@ +package ai.timefold.solver.core.impl.score.director; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.List; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; +import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.config.solver.termination.TerminationConfig; +import ai.timefold.solver.core.testdomain.TestdataEntity; +import ai.timefold.solver.core.testdomain.TestdataSolution; +import ai.timefold.solver.core.testdomain.TestdataValue; + +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.Test; + +/** + * Integration test demonstrating the use of ConstraintProvider instances. + */ +class ConstraintProviderInstanceIntegrationTest { + + @Test + void solverWithConstraintProviderInstance() { + // Create a custom constraint provider instance with runtime configuration + var customConstraintProvider = new CustomizableConstraintProvider(true, 5); + + // Build solver config with the instance + var solverConfig = new SolverConfig() + .withSolutionClass(TestdataSolution.class) + .withEntityClasses(TestdataEntity.class) + .withScoreDirectorFactory(new ScoreDirectorFactoryConfig() + .withConstraintProvider(customConstraintProvider)) + .withTerminationConfig( + new TerminationConfig().withBestScoreLimit("0").withSpentLimit(Duration.ofSeconds(30))); + + var solverFactory = SolverFactory. create(solverConfig); + var solver = solverFactory.buildSolver(); + + // Prepare test data + var solution = new TestdataSolution("solution"); + solution.setValueList(List.of(new TestdataValue("v1"), new TestdataValue("v2"))); + solution.setEntityList(List.of(new TestdataEntity("e1"), new TestdataEntity("e2"), new TestdataEntity("e3"))); + + // Solve and verify + var solvedSolution = solver.solve(solution); + assertThat(solvedSolution).isNotNull(); + assertThat(solvedSolution.getScore()).isNotNull(); + } + + @Test + void solverWithDifferentConstraintProviderInstances() { + // Test with strict configuration + var strictProvider = new CustomizableConstraintProvider(true, 10); + var strictSolver = createSolver(strictProvider); + + // Test with lenient configuration + var lenientProvider = new CustomizableConstraintProvider(false, 1); + var lenientSolver = createSolver(lenientProvider); + + // Both solvers should work with different constraint configurations + var solution = createTestSolution(); + + var strictResult = strictSolver.solve(solution); + var lenientResult = lenientSolver.solve(solution); + + assertThat(strictResult.getScore()).isNotNull(); + assertThat(lenientResult.getScore()).isNotNull(); + + // Strict provider penalizes more, so score should be worse (more negative) + assertThat(strictResult.getScore().score()).isLessThanOrEqualTo(lenientResult.getScore().score()); + } + + private ai.timefold.solver.core.api.solver.Solver createSolver( + ConstraintProvider constraintProvider) { + var solverConfig = new SolverConfig() + .withSolutionClass(TestdataSolution.class) + .withEntityClasses(TestdataEntity.class) + .withScoreDirectorFactory(new ScoreDirectorFactoryConfig() + .withConstraintProvider(constraintProvider)) + .withTerminationConfig( + new TerminationConfig().withBestScoreLimit("0").withSpentLimit(Duration.ofSeconds(30))); + return SolverFactory. create(solverConfig).buildSolver(); + } + + private TestdataSolution createTestSolution() { + var solution = new TestdataSolution("solution"); + solution.setValueList(List.of(new TestdataValue("v1"), new TestdataValue("v2"))); + solution.setEntityList(List.of(new TestdataEntity("e1"), new TestdataEntity("e2"), new TestdataEntity("e3"))); + return solution; + } + + /** + * Example of a configurable constraint provider that can be customized at + * runtime. + */ + public static class CustomizableConstraintProvider implements ConstraintProvider { + + private final boolean strictMode; + private final int penaltyWeight; + + public CustomizableConstraintProvider(boolean strictMode, int penaltyWeight) { + this.strictMode = strictMode; + this.penaltyWeight = penaltyWeight; + } + + @Override + public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + if (strictMode) { + return new Constraint[] { + strictConstraint(constraintFactory), + additionalConstraint(constraintFactory) + }; + } else { + return new Constraint[] { + strictConstraint(constraintFactory) + }; + } + } + + private Constraint strictConstraint(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(TestdataEntity.class) + .filter(entity -> entity.getValue() == null) + .penalize(SimpleScore.ONE.multiply(penaltyWeight)) + .asConstraint("Strict constraint"); + } + + private Constraint additionalConstraint(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(TestdataEntity.class) + .penalize(SimpleScore.ONE) + .asConstraint("Additional constraint"); + } + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java index 6c8d1d1c2b3..69cacd6b291 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java @@ -33,10 +33,11 @@ void incrementalScoreCalculatorWithCustomProperties() { config.setIncrementalScoreCalculatorCustomProperties(customProperties); var scoreDirectorFactory = - (IncrementalScoreDirectorFactory) buildTestdataScoreDirectoryFactory(config); + (IncrementalScoreDirectorFactory) buildTestdataScoreDirectoryFactory( + config); try (var scoreDirector = scoreDirectorFactory.buildScoreDirector()) { - var scoreCalculator = - (TestCustomPropertiesIncrementalScoreCalculator) scoreDirector.getIncrementalScoreCalculator(); + var scoreCalculator = (TestCustomPropertiesIncrementalScoreCalculator) scoreDirector + .getIncrementalScoreCalculator(); assertThat(scoreCalculator.getStringProperty()).isEqualTo("string 1"); assertThat(scoreCalculator.getIntProperty()).isEqualTo(7); } @@ -50,16 +51,17 @@ void buildWithAssertionScoreDirectorFactory() { .withIncrementalScoreCalculatorClass(TestCustomPropertiesIncrementalScoreCalculator.class) .withAssertionScoreDirectorFactory(assertionScoreDirectorConfig); - var scoreDirectorFactory = - (AbstractScoreDirectorFactory) buildTestdataScoreDirectoryFactory(config, - EnvironmentMode.STEP_ASSERT); + var scoreDirectorFactory = (AbstractScoreDirectorFactory) buildTestdataScoreDirectoryFactory( + config, + EnvironmentMode.STEP_ASSERT); var assertionScoreDirectorFactory = (IncrementalScoreDirectorFactory) scoreDirectorFactory .getAssertionScoreDirectorFactory(); try (var assertionScoreDirector = assertionScoreDirectorFactory.buildScoreDirector()) { var assertionScoreCalculator = assertionScoreDirector.getIncrementalScoreCalculator(); - assertThat(assertionScoreCalculator).isExactlyInstanceOf(TestCustomPropertiesIncrementalScoreCalculator.class); + assertThat(assertionScoreCalculator) + .isExactlyInstanceOf(TestCustomPropertiesIncrementalScoreCalculator.class); } } @@ -69,19 +71,20 @@ void multipleScoreCalculations_throwsException() { .withConstraintProviderClass(TestdataConstraintProvider.class) .withEasyScoreCalculatorClass(TestCustomPropertiesEasyScoreCalculator.class) .withIncrementalScoreCalculatorClass(TestCustomPropertiesIncrementalScoreCalculator.class); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> buildTestdataScoreDirectoryFactory(config)) + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> buildTestdataScoreDirectoryFactory(config)) .withMessageContaining("scoreDirectorFactory") .withMessageContaining("together"); } - private ScoreDirectorFactory - buildTestdataScoreDirectoryFactory(ScoreDirectorFactoryConfig config, EnvironmentMode environmentMode) { + private ScoreDirectorFactory buildTestdataScoreDirectoryFactory( + ScoreDirectorFactoryConfig config, EnvironmentMode environmentMode) { return new ScoreDirectorFactoryFactory(config) .buildScoreDirectorFactory(environmentMode, TestdataSolution.buildSolutionDescriptor()); } - private ScoreDirectorFactory - buildTestdataScoreDirectoryFactory(ScoreDirectorFactoryConfig config) { + private ScoreDirectorFactory buildTestdataScoreDirectoryFactory( + ScoreDirectorFactoryConfig config) { return buildTestdataScoreDirectoryFactory(config, EnvironmentMode.PHASE_ASSERT); } @@ -89,12 +92,50 @@ void multipleScoreCalculations_throwsException() { void constraintStreamsBavet() { var config = new ScoreDirectorFactoryConfig() .withConstraintProviderClass(TestdataConstraintProvider.class); - var scoreDirectorFactory = - BavetConstraintStreamScoreDirectorFactory.buildScoreDirectorFactory(TestdataSolution.buildSolutionDescriptor(), - config, EnvironmentMode.PHASE_ASSERT); + var scoreDirectorFactory = BavetConstraintStreamScoreDirectorFactory.buildScoreDirectorFactory( + TestdataSolution.buildSolutionDescriptor(), + config, EnvironmentMode.PHASE_ASSERT); assertThat(scoreDirectorFactory).isInstanceOf(BavetConstraintStreamScoreDirectorFactory.class); } + @Test + void constraintProviderInstance() { + var constraintProviderInstance = new TestdataConstraintProvider(); + var config = new ScoreDirectorFactoryConfig() + .withConstraintProvider(constraintProviderInstance); + var scoreDirectorFactory = BavetConstraintStreamScoreDirectorFactory.buildScoreDirectorFactory( + TestdataSolution.buildSolutionDescriptor(), + config, EnvironmentMode.PHASE_ASSERT); + assertThat(scoreDirectorFactory).isInstanceOf(BavetConstraintStreamScoreDirectorFactory.class); + } + + @Test + void constraintProviderInstanceAndClass_throwsException() { + var constraintProviderInstance = new TestdataConstraintProvider(); + var config = new ScoreDirectorFactoryConfig() + .withConstraintProvider(constraintProviderInstance) + .withConstraintProviderClass(TestdataConstraintProvider.class); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> buildTestdataScoreDirectoryFactory(config)) + .withMessageContaining("cannot have both") + .withMessageContaining("constraintProviderClass") + .withMessageContaining("constraintProvider instance"); + } + + @Test + void constraintProviderInstanceWithCustomProperties_throwsException() { + var constraintProviderInstance = new TestdataConstraintProvider(); + var customProperties = new HashMap(); + customProperties.put("someProperty", "someValue"); + var config = new ScoreDirectorFactoryConfig() + .withConstraintProvider(constraintProviderInstance) + .withConstraintProviderCustomProperties(customProperties); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> buildTestdataScoreDirectoryFactory(config)) + .withMessageContaining("constraintProviderCustomProperties") + .withMessageContaining("cannot be used when a constraintProvider instance is provided"); + } + public static class TestCustomPropertiesEasyScoreCalculator implements EasyScoreCalculator { diff --git a/docs/src/antora.yml b/docs/src/antora.yml index 3f2a450dff9..cd1aa2fce67 100644 --- a/docs/src/antora.yml +++ b/docs/src/antora.yml @@ -3,17 +3,17 @@ # That file is then copied to src/modules/antora.yml and committed to Git on the release branch. # The timefold.ai website can then be refreshed from the release branch and/or tag. name: timefold-solver -title: Timefold Solver ${project.version} +title: Timefold Solver 1.28.0 version: latest asciidoc: attributes: - timefold-solver-version: ${project.version} - java-version: ${maven.compiler.release} - maven-version: ${maven.min.version} - quarkus-version: ${version.io.quarkus} - spring-boot-version: ${version.org.springframework.boot} - logback-version: ${version.ch.qos.logback} - exec-maven-plugin-version: ${version.exec.plugin} - rewrite-maven-plugin-version: ${version.rewrite.plugin} + timefold-solver-version: 1.28.0 + java-version: 17 + maven-version: 3.9.11 + quarkus-version: 3.28.5 + spring-boot-version: 3.5.7 + logback-version: 1.5.20 + exec-maven-plugin-version: 3.6.2 + rewrite-maven-plugin-version: 6.23.0 nav: - modules/ROOT/nav.adoc diff --git a/pom.xml b/pom.xml index 55e41b256d3..57524276ec4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,7 @@ - + 4.0.0 ai.timefold.solver @@ -27,6 +28,8 @@ + + ${env.GITHUB_REPOSITORY} 3.12.0 6.23.0 3.3.1 @@ -70,6 +73,14 @@ migration + + + github + GitHub Packages + https://maven.pkg.github.com/${github.repository} + + + @@ -167,7 +178,8 @@ @@ -190,4 +202,4 @@ - + \ No newline at end of file diff --git a/timefold-solver.code-workspace b/timefold-solver.code-workspace new file mode 100644 index 00000000000..e854c0c0e5d --- /dev/null +++ b/timefold-solver.code-workspace @@ -0,0 +1,104 @@ +{ + "folders": [ + { + "path": "documentation", + "name": "ⓘ Docs" + }, + { + "path": ".gitlab-ci", + "name": "đŸ› ī¸ GitLab CI" + }, + { + "path": ".", + "name": "🚀 optimization-service" + } + ], + "settings": { + "coverage-gutters.showGutterCoverage": true, + "coverage-gutters.showLineCoverage": true, + "debug.inlineValues": "on", + "editor.formatOnSave": true, + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features", + "editor.insertSpaces": true, + "editor.tabSize": 4 + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features", + "editor.insertSpaces": true, + "editor.tabSize": 4 + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": "active", + "asciidoc.preview.asciidoctorAttributes": { + "imagesdir": "../images" + }, + "todo-tree.general.tags": [ + "FIXME", + "HACK", + "TODO", + "REVIEW", + "IDEA" + ], + "todo-tree.general.tagGroups": { + "ToDo": [ + "TODO" + ], + "FixMe": [ + "FIXME", + "HACK", + "BUG" + ], + "Review": [ + "REVIEW" + ], + "Idea": [ + "IDEA" + ] + }, + "local-history.path": "${workspaceFolder:}", + "todo-tree.general.rootFolder": "", + "todo-tree.filtering.includeHiddenFiles": false, + "todo-tree.filtering.excludeGlobs": [ + "**/target/*/**" + ], + "coverage-gutters.coverageFileNames": [ + "target/jacoco-report/jacoco.xml" + ], + "yaml.schemas": { + "https://gitlab.com/gitlab-org/gitlab-foss/-/raw/master/app/assets/javascripts/editor/schema/ci.json": "file:///c%3A/projects/node-service-template/.gitlab-ci/**/*.yml" + }, + "window.title": "${rootName}${separator}${rootPath}${separator}${profileName}${separator}${appName}", + "java.configuration.updateBuildConfiguration": "automatic", + "microprofile.tools.validation.unassigned.excluded": [ + "rre.api.access-token" + ], + "java.diagnostic.filter": [ + "**/target/generated-sources/**/*" + ], + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx16G -Xms100m -Xlog:disable", + "chat.agent.maxRequests": 300, + "chat.tools.terminal.enableAutoApprove": true, + "chat.tools.terminal.autoApprove": { + "rm": false, + "rmdir": false, + "del": false, + "kill": false, + "curl": true, + "wget": true, + "eval": false, + "chmod": false, + "chown": false, + "/^Remove-Item\\b/i": false + }, + "java.debug.settings.onBuildFailureProceed": true + }, + "extensions": { + "recommendations": [] + } +} \ No newline at end of file