Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2a23820fa | |||
| 8c9dbc90ec | |||
| 6d01b5824f | |||
| 142e385cf2 | |||
| b89acfb2bd | |||
| 854ba1f8a8 | |||
| 073bdd3751 | |||
| 20b049abed | |||
| c08dd4bff0 | |||
| 8cc6da4b98 | |||
| 29821baed6 | |||
| 56eb92e090 | |||
| fd557bc45c | |||
| 63f3a355d9 | |||
| becfe765f5 | |||
| 82be8ffefc | |||
| f01f797296 | |||
| 8b067983ed | |||
| eb98fa0bb5 | |||
| c64780be71 | |||
| 3b21a1882b | |||
| 7e70435f48 | |||
| 14209bf730 | |||
| 31eee9fae5 | |||
| 977b7b23cc |
@@ -0,0 +1,30 @@
|
||||
name: Build Release
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Write settings.xml
|
||||
if: ${{ env.RELEASE_TRAIN_SETTINGS_XML != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::notice::Writing provided settings-xml input to release-train-settings.xml"
|
||||
echo "${RELEASE_TRAIN_SETTINGS_XML}" > release-train-settings.xml
|
||||
- name: Prepare Maven command
|
||||
shell: bash
|
||||
run: |
|
||||
MAVEN_LOGGING_OPTS=()
|
||||
if [[ "${RUNNER_DEBUG}" == "1" || "${ACTIONS_STEP_DEBUG}" == "true" ]]; then
|
||||
echo "::notice::transfer-progress enabled"
|
||||
else
|
||||
MAVEN_LOGGING_OPTS+=(--no-transfer-progress)
|
||||
fi
|
||||
|
||||
echo "RELEASE_TRAIN_MVN=./mvnw --batch-mode ${MAVEN_LOGGING_OPTS[*]} --settings release-train-settings.xml clean deploy -DaltDeploymentRepository=release-train::file://$(pwd)/deployment-repository -DskipTests -Dgpg.skip" >> "${GITHUB_ENV}"
|
||||
- name: Build Release
|
||||
shell: bash
|
||||
run: ${RELEASE_TRAIN_MVN} --activate-profiles releaseTrain,release,ci
|
||||
- name: Distribution build
|
||||
shell: bash
|
||||
run: ${RELEASE_TRAIN_MVN} --activate-profiles releaseTrain,distribute
|
||||
- name: Schema build
|
||||
shell: bash
|
||||
run: ${RELEASE_TRAIN_MVN} --activate-profiles releaseTrain,distribute-schema
|
||||
@@ -0,0 +1,6 @@
|
||||
artifactory:
|
||||
artifacts:
|
||||
- pattern: "/**/spring-data-*-docs.zip"
|
||||
properties:
|
||||
zip.deployed: "false"
|
||||
zip.type: "docs"
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Test Release
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Write settings.xml
|
||||
if: ${{ env.RELEASE_TRAIN_SETTINGS_XML != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::notice::Writing provided settings-xml input to release-train-settings.xml"
|
||||
echo "${RELEASE_TRAIN_SETTINGS_XML}" > release-train-settings.xml
|
||||
- name: Prepare Maven command
|
||||
shell: bash
|
||||
run: |
|
||||
MAVEN_LOGGING_OPTS=()
|
||||
if [[ "${RUNNER_DEBUG}" == "1" || "${ACTIONS_STEP_DEBUG}" == "true" ]]; then
|
||||
echo "::notice::transfer-progress enabled"
|
||||
else
|
||||
MAVEN_LOGGING_OPTS+=(--no-transfer-progress)
|
||||
fi
|
||||
|
||||
echo "RELEASE_TRAIN_MVN=./mvnw --batch-mode ${MAVEN_LOGGING_OPTS[*]} --settings release-train-settings.xml clean verify -Dgpg.skip" >> "${GITHUB_ENV}"
|
||||
- name: Test Release
|
||||
shell: bash
|
||||
run: ${RELEASE_TRAIN_MVN} --activate-profiles releaseTrain,release,ci
|
||||
@@ -0,0 +1,26 @@
|
||||
workflow:
|
||||
generator:
|
||||
project:
|
||||
java:
|
||||
versions:
|
||||
primary: 25
|
||||
workflows:
|
||||
release-train:
|
||||
join:
|
||||
watch: false
|
||||
leave:
|
||||
watch: false
|
||||
ready:
|
||||
watch: false
|
||||
build:
|
||||
env:
|
||||
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
|
||||
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
|
||||
COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL
|
||||
RELEASE_TRAIN_SETTINGS_XML: vars.RELEASE_TRAIN_SETTINGS_XML
|
||||
test:
|
||||
env:
|
||||
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
|
||||
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
|
||||
COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL
|
||||
RELEASE_TRAIN_SETTINGS_XML: vars.RELEASE_TRAIN_SETTINGS_XML
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
matrix:
|
||||
java-version: [ base, main ]
|
||||
name: Build project
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ vars.ACTION_RUNNER || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Java and Maven
|
||||
@@ -24,6 +24,10 @@ jobs:
|
||||
- name: Build
|
||||
uses: spring-projects/spring-data-build/actions/maven-build@main
|
||||
env:
|
||||
OSS_ARTIFACTORY_USR: '${{ secrets.ARTIFACTORY_USERNAME }}'
|
||||
OSS_ARTIFACTORY_PSW: '${{ secrets.ARTIFACTORY_PASSWORD }}'
|
||||
COMMERCIAL_ARTIFACTORY_USR: '${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}'
|
||||
COMMERCIAL_ARTIFACTORY_PSW: '${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}'
|
||||
TESTCONTAINERS_REUSE_ENABLE: true
|
||||
with:
|
||||
settings-xml: '${{ vars.SETTINGS_XML }}'
|
||||
|
||||
@@ -4,6 +4,7 @@ name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Build"
|
||||
run-name: "${{ inputs.callback-ref }} – Build"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
callback:
|
||||
description: "Repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
callback-ref:
|
||||
description: "Ref in the callback repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-maven-repository-url:
|
||||
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "read"
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
jobs:
|
||||
build-release:
|
||||
name: "Build Release"
|
||||
runs-on: "ubuntu22-2-8"
|
||||
steps:
|
||||
- name: "Prevent Re-runs"
|
||||
id: "prevent-re-runs"
|
||||
run: |-
|
||||
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
|
||||
echo "Re-runs are prohibited. Use the 'Release Train – Retry' workflow to retry build failures"
|
||||
exit 1
|
||||
fi
|
||||
- name: "Set up Java"
|
||||
id: "set-up-java"
|
||||
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
|
||||
with:
|
||||
distribution: "liberica"
|
||||
java-version: "25"
|
||||
- name: "Check Out Code"
|
||||
id: "check-out-code"
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
- name: "Build Release"
|
||||
id: "build-release"
|
||||
uses: "./.github/actions/release-train-build"
|
||||
env:
|
||||
COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}"
|
||||
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
|
||||
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
|
||||
RELEASE_TRAIN_SETTINGS_XML: "${{ vars.RELEASE_TRAIN_SETTINGS_XML }}"
|
||||
- name: "Upload Deployment Repository"
|
||||
id: "upload-deployment-repository"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
name: "deployment-repository"
|
||||
path: "deployment-repository/**"
|
||||
- name: "Upload Deployment Spec"
|
||||
id: "upload-deployment-spec"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
archive: "false"
|
||||
if-no-files-found: "ignore"
|
||||
name: "deployment-spec"
|
||||
path: ".github/actions/release-train-build/deployment-spec.yml"
|
||||
- name: "Save Build System Caches"
|
||||
id: "save-build-system-caches"
|
||||
uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
|
||||
with:
|
||||
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
|
||||
path: "~/.m2/repository"
|
||||
- name: "Send Callback"
|
||||
id: "send-callback"
|
||||
if: "${{ !cancelled() }}"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run callback \
|
||||
--repo ${{ inputs.callback }} \
|
||||
--ref ${{ inputs.callback-ref }} \
|
||||
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
|
||||
--field deployment-repository-artifact-identifier=${{ steps.upload-deployment-repository.outputs.artifact-id }} \
|
||||
--field deployment-spec-artifact-identifier=${{ steps.upload-deployment-spec.outputs.artifact-id }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }} \
|
||||
--field result=${{ job.status == 'success' && 'built' || 'build-failed' }} \
|
||||
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@@ -0,0 +1,43 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Join"
|
||||
run-name: "${{ inputs.release-train }} – Join"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
deployment-destination:
|
||||
description: "Destination to which the release should be deployed"
|
||||
options:
|
||||
- "Maven Central"
|
||||
- "Spring Enterprise"
|
||||
required: true
|
||||
type: "choice"
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
join-release-train:
|
||||
name: "Join Release Train"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Join Release Train"
|
||||
id: "join-release-train"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run join \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field commit-hash=${{ github.sha }} \
|
||||
--field deployment-destination=${{ inputs.deployment-destination == 'Maven Central' && 'maven-central' || 'spring-enterprise' }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
@@ -0,0 +1,34 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Leave"
|
||||
run-name: "${{ inputs.release-train }} – Leave"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
leave:
|
||||
name: "Leave"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Leave"
|
||||
id: "leave"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run leave \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
@@ -0,0 +1,35 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Ready"
|
||||
run-name: "${{ inputs.release-train }} – Ready"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
ready:
|
||||
name: "Ready"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Ready"
|
||||
id: "ready"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run ready \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field commit-hash=${{ github.sha }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
@@ -0,0 +1,34 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Retry"
|
||||
run-name: "${{ inputs.release-train }} – Retry"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
trigger-retry:
|
||||
name: "Trigger Retry"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Trigger Retry"
|
||||
id: "trigger-retry"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run retry \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
@@ -0,0 +1,85 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Test"
|
||||
run-name: "${{ inputs.callback-ref }} – Test"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
callback:
|
||||
description: "Repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
callback-ref:
|
||||
description: "Ref in the callback repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-maven-repository-url:
|
||||
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "read"
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
jobs:
|
||||
test-release:
|
||||
name: "Test Release"
|
||||
runs-on: "ubuntu22-2-8"
|
||||
steps:
|
||||
- name: "Prevent Re-runs"
|
||||
id: "prevent-re-runs"
|
||||
run: |-
|
||||
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
|
||||
echo "Re-runs are prohibited. Use the 'Release Train – Retry' workflow to retry test failures"
|
||||
exit 1
|
||||
fi
|
||||
- name: "Set up Java"
|
||||
id: "set-up-java"
|
||||
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
|
||||
with:
|
||||
distribution: "liberica"
|
||||
java-version: "25"
|
||||
- name: "Check Out Code"
|
||||
id: "check-out-code"
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
- name: "Restore Build System Caches"
|
||||
id: "restore-build-system-caches"
|
||||
uses: "actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
|
||||
with:
|
||||
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
|
||||
path: "~/.m2/repository"
|
||||
- name: "Test Release"
|
||||
id: "test-release"
|
||||
uses: "./.github/actions/release-train-test"
|
||||
env:
|
||||
COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}"
|
||||
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
|
||||
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
|
||||
RELEASE_TRAIN_SETTINGS_XML: "${{ vars.RELEASE_TRAIN_SETTINGS_XML }}"
|
||||
- name: "Send Callback"
|
||||
id: "send-callback"
|
||||
if: "${{ !cancelled() }}"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run callback \
|
||||
--repo ${{ inputs.callback }} \
|
||||
--ref ${{ inputs.callback-ref }} \
|
||||
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }} \
|
||||
--field result=${{ job.status == 'success' && 'tested' || 'test-failed' }} \
|
||||
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
- name: "Upload Build System Reports"
|
||||
id: "upload-build-system-reports"
|
||||
if: "${{ failure() }}"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
name: "build-system-reports"
|
||||
path: |-
|
||||
**/target/surefire-reports
|
||||
**/target/failsafe-reports
|
||||
@@ -3,7 +3,7 @@ name: Snapshots
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main, 'issue/**' ]
|
||||
branches: [ main, 'issue/**', 'release/**' ]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
build-snapshots:
|
||||
name: Build and deploy snapshots
|
||||
if: ${{ github.repository_owner == 'spring-projects' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ vars.ACTION_RUNNER || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup Java and Maven
|
||||
@@ -21,12 +21,11 @@ jobs:
|
||||
- name: Deploy to Artifactory
|
||||
uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@main
|
||||
env:
|
||||
OSS_ARTIFACTORY_USR: '${{ secrets.ARTIFACTORY_USERNAME }}'
|
||||
OSS_ARTIFACTORY_PSW: '${{ secrets.ARTIFACTORY_PASSWORD }}'
|
||||
COMMERCIAL_ARTIFACTORY_USR: '${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}'
|
||||
COMMERCIAL_ARTIFACTORY_PSW: '${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}'
|
||||
TESTCONTAINERS_REUSE_ENABLE: true
|
||||
with:
|
||||
build-name: 'spring-data-elasticsearch'
|
||||
username: '${{ secrets.ARTIFACTORY_USERNAME }}'
|
||||
password: '${{ secrets.ARTIFACTORY_PASSWORD }}'
|
||||
context-url: '${{ vars.ARTIFACTORY_CONTEXT_URL }}'
|
||||
repository: '${{ vars.ARTIFACTORY_REPOSITORY }}'
|
||||
project: '${{ vars.ARTIFACTORY_PROJECT }}'
|
||||
settings-xml: '${{ vars.SETTINGS_XML }}'
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
= Security Policy
|
||||
|
||||
== Reporting a Vulnerability
|
||||
|
||||
Please, https://github.com/spring-projects/security-advisories/security/advisories/new[open a draft security advisory] if you need to disclose and discuss a security issue in private with the Spring Data team.
|
||||
Note that we only accept reports against https://spring.io/projects/spring-data#support[supported versions].
|
||||
|
||||
For more details, check out our https://spring.io/security-policy[security policy].
|
||||
|
||||
== JAR signing
|
||||
|
||||
Spring Data JARs released on Maven Central are signed.
|
||||
You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt
|
||||
|
||||
Versions released prior to 2023 may be signed with a different key.
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please [open a draft security advisory](https://github.com/spring-projects/spring-data-elasticsearch/security/advisories/new) if you need to disclose and discuss a security issue in private with the Spring Data team.
|
||||
Note that we only accept reports against [supported versions](https://spring.io/projects/spring-data#support).
|
||||
|
||||
For more details, check out our [security policy](https://spring.io/security-policy).
|
||||
|
||||
## JAR signing
|
||||
|
||||
Spring Data JARs released on Maven Central are signed.
|
||||
You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt
|
||||
|
||||
Versions released prior to 2023 may be signed with a different key.
|
||||
@@ -0,0 +1,11 @@
|
||||
= Getting support for Spring Data Elasticsearch
|
||||
|
||||
Spring Data Elasticsearch is a community-maintained Spring Data project. It is released as part of the Spring Data Release train.
|
||||
|
||||
== GitHub issues
|
||||
|
||||
We choose not to use GitHub issues for general usage questions and support, preferring to use issues solely for the tracking of bugs and enhancements.
|
||||
If you have a general usage question, please do not open a GitHub issue but use one of the other channels https://github.com/spring-projects/spring-data-elasticsearch#getting-help[described in the readme file].
|
||||
|
||||
If you are reporting a bug, please help to speed up problem diagnosis by providing as much information as possible.
|
||||
Ideally, that would include a small sample project that reproduces the problem.
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-elasticsearch</artifactId>
|
||||
<version>6.1.0</version>
|
||||
<version>6.2.0-M1</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.build</groupId>
|
||||
<artifactId>spring-data-parent</artifactId>
|
||||
<version>4.1.0</version>
|
||||
<version>4.2.0-M1</version>
|
||||
</parent>
|
||||
|
||||
<name>Spring Data Elasticsearch</name>
|
||||
@@ -18,11 +18,11 @@
|
||||
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
|
||||
|
||||
<properties>
|
||||
<springdata.commons>4.1.0</springdata.commons>
|
||||
<springdata.commons>4.2.0-M1</springdata.commons>
|
||||
|
||||
<!-- version of the ElasticsearchClient -->
|
||||
<elasticsearch-java>9.4.2</elasticsearch-java>
|
||||
<elasticsearch-rest-client>9.4.2</elasticsearch-rest-client>
|
||||
<elasticsearch-java>9.5.0</elasticsearch-java>
|
||||
<elasticsearch-rest-client>9.5.0</elasticsearch-rest-client>
|
||||
|
||||
<hoverfly>0.20.2</hoverfly>
|
||||
<log4j>2.25.4</log4j>
|
||||
@@ -376,8 +376,18 @@
|
||||
<filtering>false</filtering>
|
||||
<excludes>
|
||||
<exclude>**/versions.properties</exclude>
|
||||
<exclude>notice.txt</exclude>
|
||||
<exclude>license.txt</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>${project.root}/src/main/resources</directory>
|
||||
<targetPath>META-INF</targetPath>
|
||||
<includes>
|
||||
<include>notice.txt</include>
|
||||
<include>license.txt</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
This chapter illustrates configuration and usage of supported Elasticsearch client implementations.
|
||||
|
||||
Spring Data Elasticsearch operates upon an Elasticsearch client (provided by Elasticsearch client libraries) that is connected to a single Elasticsearch node or a cluster.
|
||||
Spring Data Elasticsearch operates upon an Elasticsearch client (provided by Elasticsearch client libraries) that is connected to a single Elasticsearch node, a cluster or Elasticsearch serverless.
|
||||
Although the Elasticsearch Client can be used directly to work with the cluster, applications using Spring Data Elasticsearch normally use the higher level abstractions of xref:elasticsearch/template.adoc[Elasticsearch Operations] and xref:elasticsearch/repositories/elasticsearch-repositories.adoc[Elasticsearch Repositories].
|
||||
|
||||
[[elasticsearch.clients.rest5client]]
|
||||
@@ -454,6 +454,22 @@ ClientConfiguration.builder()
|
||||
----
|
||||
====
|
||||
|
||||
[[elasticsearch.clients.servertype]]
|
||||
== Server Type
|
||||
There are now 2 types of server that a client could connect to. They are `DEFAULT` which represent all single nodes and clusters; and `SERVERLESS` which represents Elasticsearch new flavour of providing cloud hosted elasticsearch.
|
||||
If you are going to use Elasticsearch Serverless then you need to specific this so that certain settings are changed and indexes are created correctly.
|
||||
The javadoc:org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport[] class (extended by javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[]) allows the overriding of `serverType()`.
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
ElasticsearchServerType serverType() {
|
||||
return ElasticsearchServerType.SERVERLESS;
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[elasticsearch.clients.logging]]
|
||||
== Client Logging
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
[[new-features]]
|
||||
= What's new
|
||||
|
||||
[[new-features.6-2-0]]
|
||||
== New in Spring Data Elasticsearch 6.2
|
||||
|
||||
* Upgrade to Elasticsearch 9.5.0
|
||||
* Allow to provide non-standard (custom) index options for `@Field` / `@InnerField`
|
||||
* Support Elasticsearch Serverless
|
||||
* Support propert resolution in index name expressions (https://github.com/spring-projects/spring-data-elasticsearch/issues/3310[GH #3310])
|
||||
|
||||
[[new-features.6-1-0]]
|
||||
== New in Spring Data Elasticsearch 6.1
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ The following annotations are available:
|
||||
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database.
|
||||
The most important attributes are (check the API documentation for the complete list of attributes):
|
||||
** `indexName`: the name of the index to store this entity in.
|
||||
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"`
|
||||
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"` or a property expression like `{index.name}`
|
||||
** `createIndex`: flag whether to create an index on repository bootstrapping.
|
||||
Default value is _true_.
|
||||
See xref:elasticsearch/repositories/elasticsearch-repositories.adoc#elasticsearch.repositories.autocreation[Automatic creation of indices with the corresponding mapping]
|
||||
|
||||
@@ -6,6 +6,7 @@ The following table shows the Elasticsearch and Spring versions that are used by
|
||||
[cols="^,^,^,^",options="header"]
|
||||
|===
|
||||
| Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework
|
||||
| 2026.1 | 6.2.x | 9.5.0 | 7.0.x
|
||||
| 2026.0 | 6.1.x | 9.4.2 | 7.0.x
|
||||
| 2025.1 | 6.0.x | 9.2.2 | 7.0.x
|
||||
| 2025.0 | 5.5.xfootnote:oom[Out of maintenance] | 8.18.1 | 6.2.x
|
||||
|
||||
@@ -13,7 +13,7 @@ asciidoc:
|
||||
include-xml-namespaces: false
|
||||
spring-data-commons-docs-url: '${documentation.baseurl}/spring-data/commons/reference/${springdata.commons.short}'
|
||||
spring-data-commons-javadoc-base: '{spring-data-commons-docs-url}/api/java'
|
||||
springdocsurl: '${documentation.baseurl}/spring-framework/reference/{springversionshort}'
|
||||
springdocsurl: '${documentation.spring-reference-url}/{springversionshort}'
|
||||
spring-framework-docs: '{springdocsurl}'
|
||||
springjavadocurl: '${documentation.spring-javadoc-url}'
|
||||
spring-framework-javadoc: '{springjavadocurl}'
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2026-present 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.data.elasticsearch.annotations;
|
||||
|
||||
import org.springframework.data.elasticsearch.core.index.IndexOptionMapper;
|
||||
|
||||
/**
|
||||
* Represents the custom index option, either not provided by the core (fe, custom plugin) or deviates across different
|
||||
* engines.
|
||||
*
|
||||
* @author Andriy Redko
|
||||
* @since 6.2
|
||||
*/
|
||||
public @interface CustomIndexOption {
|
||||
/**
|
||||
* The name of the custom index option
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* The value(s) of the custom index option
|
||||
*/
|
||||
String[] values() default {};
|
||||
|
||||
/**
|
||||
* Should the index property be overridden if already present or not
|
||||
*/
|
||||
boolean overrideIfPresent() default false;
|
||||
|
||||
/**
|
||||
* The index option mapper that will be used to populate this custom index option
|
||||
*/
|
||||
Class<? extends IndexOptionMapper> mapper();
|
||||
}
|
||||
@@ -252,4 +252,11 @@ public @interface Field {
|
||||
* @since 5.4
|
||||
*/
|
||||
String mappedTypeName() default "";
|
||||
|
||||
/**
|
||||
* adds the custom index options for a particular field
|
||||
*
|
||||
* @since 6.2
|
||||
*/
|
||||
CustomIndexOption[] customIndexOptions() default {};
|
||||
}
|
||||
|
||||
@@ -179,4 +179,11 @@ public @interface InnerField {
|
||||
* @since 5.4
|
||||
*/
|
||||
String mappedTypeName() default "";
|
||||
|
||||
/**
|
||||
* adds the custom index options for a particular field
|
||||
*
|
||||
* @since 6.2
|
||||
*/
|
||||
CustomIndexOption[] customIndexOptions() default {};
|
||||
}
|
||||
|
||||
@@ -22,12 +22,14 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.data.annotation.Persistent;
|
||||
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchPersistentEntity;
|
||||
|
||||
/**
|
||||
* Elasticsearch Setting
|
||||
*
|
||||
* @author Mohsin Husen
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Steven Pearce
|
||||
*/
|
||||
|
||||
@Persistent
|
||||
@@ -59,9 +61,10 @@ public @interface Setting {
|
||||
short replicas() default 1;
|
||||
|
||||
/**
|
||||
* Refresh interval for the index. Used for index creation.
|
||||
* Refresh interval for the index. Used for index creation. If no value, defaults are server type dependant and set in
|
||||
* {@link SimpleElasticsearchPersistentEntity}
|
||||
*/
|
||||
String refreshInterval() default "1s";
|
||||
String refreshInterval() default "";
|
||||
|
||||
/**
|
||||
* Index storage type for the index. Used for index creation.
|
||||
|
||||
+64
-17
@@ -24,12 +24,18 @@ import co.elastic.clients.elasticsearch.core.search.Hit;
|
||||
import co.elastic.clients.elasticsearch.core.search.NestedIdentity;
|
||||
import co.elastic.clients.json.JsonData;
|
||||
import co.elastic.clients.json.JsonpMapper;
|
||||
import jakarta.json.JsonArray;
|
||||
import jakarta.json.JsonNumber;
|
||||
import jakarta.json.JsonObject;
|
||||
import jakarta.json.JsonString;
|
||||
import jakarta.json.JsonValue;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -85,22 +91,8 @@ final class DocumentAdapters {
|
||||
|
||||
Map<String, Double> matchedQueries = hit.matchedQueries();
|
||||
|
||||
Function<Map<String, JsonData>, EntityAsMap> fromFields = fields -> {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
final boolean[] firstField = { true };
|
||||
hit.fields().forEach((key, jsonData) -> {
|
||||
if (!firstField[0]) {
|
||||
sb.append(',');
|
||||
}
|
||||
sb.append('"').append(key).append("\":") //
|
||||
.append(jsonData.toJson(jsonpMapper).toString());
|
||||
firstField[0] = false;
|
||||
});
|
||||
sb.append('}');
|
||||
return new EntityAsMap().fromJson(sb.toString());
|
||||
};
|
||||
|
||||
EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields());
|
||||
EntityAsMap hitFieldsAsMap = new EntityAsMap();
|
||||
hit.fields().forEach((key, jsonData) -> hitFieldsAsMap.put(key, toJavaObject(jsonData, jsonpMapper)));
|
||||
|
||||
Map<String, List<@Nullable Object>> documentFields = new LinkedHashMap<>();
|
||||
hitFieldsAsMap.forEach((key, value) -> {
|
||||
@@ -144,6 +136,61 @@ final class DocumentAdapters {
|
||||
documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Object toJavaObject(JsonData jsonData, JsonpMapper jsonpMapper) {
|
||||
return toJavaObject(jsonData.toJson(jsonpMapper));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Object toJavaObject(JsonValue jsonValue) {
|
||||
|
||||
return switch (jsonValue.getValueType()) {
|
||||
case OBJECT -> toMap(jsonValue.asJsonObject());
|
||||
case ARRAY -> toList(jsonValue.asJsonArray());
|
||||
case STRING -> ((JsonString) jsonValue).getString();
|
||||
case NUMBER -> toNumber((JsonNumber) jsonValue);
|
||||
case TRUE -> Boolean.TRUE;
|
||||
case FALSE -> Boolean.FALSE;
|
||||
case NULL -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private static Map<String, @Nullable Object> toMap(JsonObject jsonObject) {
|
||||
|
||||
Map<String, @Nullable Object> result = new LinkedHashMap<>();
|
||||
jsonObject.forEach((key, value) -> result.put(key, toJavaObject(value)));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<@Nullable Object> toList(JsonArray jsonArray) {
|
||||
|
||||
List<@Nullable Object> result = new ArrayList<>(jsonArray.size());
|
||||
jsonArray.forEach(value -> result.add(toJavaObject(value)));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Number toNumber(JsonNumber jsonNumber) {
|
||||
|
||||
if (!jsonNumber.isIntegral()) {
|
||||
return jsonNumber.doubleValue();
|
||||
}
|
||||
|
||||
try {
|
||||
return jsonNumber.intValueExact();
|
||||
} catch (ArithmeticException ignored) {
|
||||
// continue with a wider numeric type
|
||||
}
|
||||
|
||||
try {
|
||||
return jsonNumber.longValueExact();
|
||||
} catch (ArithmeticException ignored) {
|
||||
// continue with a wider numeric type
|
||||
}
|
||||
|
||||
BigInteger value = jsonNumber.bigIntegerValue();
|
||||
return value;
|
||||
}
|
||||
|
||||
public static SearchDocument from(CompletionSuggestOption<EntityAsMap> completionSuggestOption) {
|
||||
|
||||
Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source())
|
||||
|
||||
+12
-10
@@ -93,16 +93,18 @@ public final class Rest5Clients {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
httpAsyncClientBuilder.addRequestInterceptorFirst((request, entity, context) -> {
|
||||
clientConfiguration.getHeadersSupplier().get().forEach((header, values) -> {
|
||||
// The accept and content-type headers are already put on the request, despite this being the first
|
||||
// interceptor.
|
||||
if ("Accept".equalsIgnoreCase(header) || " Content-Type".equalsIgnoreCase(header)) {
|
||||
request.removeHeaders(header);
|
||||
}
|
||||
values.forEach(value -> request.addHeader(header, value));
|
||||
});
|
||||
});
|
||||
httpAsyncClientBuilder.addExecInterceptorFirst("es-rest5-client",
|
||||
(request, entityProducer, scope, chain, asyncExecCallback) -> {
|
||||
clientConfiguration.getHeadersSupplier().get().forEach((header, values) -> {
|
||||
// The accept and content-type headers may already be put on the request, despite this being the
|
||||
// first interceptor.
|
||||
if ("Accept".equalsIgnoreCase(header) || "Content-Type".equalsIgnoreCase(header)) {
|
||||
request.removeHeaders(header);
|
||||
}
|
||||
values.forEach(value -> request.addHeader(header, value));
|
||||
});
|
||||
chain.proceed(request, entityProducer, scope, asyncExecCallback);
|
||||
});
|
||||
|
||||
// add httpclient configurator callbacks provided by the configuration
|
||||
for (ClientConfiguration.ClientConfigurationCallback<?> clientConfigurer : clientConfiguration
|
||||
|
||||
+14
-1
@@ -42,6 +42,7 @@ import org.springframework.util.StringUtils;
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Steven Pearce
|
||||
* @since 3.2
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@@ -73,6 +74,7 @@ public class ElasticsearchConfigurationSupport {
|
||||
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder());
|
||||
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
|
||||
mappingContext.setWriteTypeHints(writeTypeHints());
|
||||
mappingContext.setServerType(serverType());
|
||||
|
||||
return mappingContext;
|
||||
}
|
||||
@@ -174,7 +176,7 @@ public class ElasticsearchConfigurationSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag specifiying if type hints (_class fields) should be written in the index. It is strongly advised to keep the
|
||||
* Flag specifying if type hints (_class fields) should be written in the index. It is strongly advised to keep the
|
||||
* default value of {@literal true}. If you need to write to an existing index that does not have a mapping defined
|
||||
* for these fields and that has a strict mapping set, then it might be necessary to disable type hints. But notice
|
||||
* that in this case reading polymorphic types may fail.
|
||||
@@ -185,4 +187,15 @@ public class ElasticsearchConfigurationSupport {
|
||||
protected boolean writeTypeHints() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the {@link ElasticsearchServerType} to use when creating indexes. The Default value will support the
|
||||
* standard configurations, and SERVERLESS will support ElasticSearch Serverless
|
||||
*
|
||||
* @return the {@link ElasticsearchServerType} to use
|
||||
* @since 6.2
|
||||
*/
|
||||
protected ElasticsearchServerType serverType() {
|
||||
return ElasticsearchServerType.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2026-present 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.data.elasticsearch.config;
|
||||
|
||||
|
||||
/**
|
||||
* ElasticsearchServerType defines the type of Elasticsearch server you are connecting to.
|
||||
* @see #DEFAULT
|
||||
* @see #SERVERLESS
|
||||
*
|
||||
* @author Steven Pearce
|
||||
* @since 6.2
|
||||
*/
|
||||
public enum ElasticsearchServerType {
|
||||
/**
|
||||
* Normal installations of Elasticsearch including cloud-hosted Elasticsearch
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* New Flavour of Elasticsearch, offered by Elastic
|
||||
*/
|
||||
SERVERLESS,
|
||||
}
|
||||
+1
-2
@@ -104,9 +104,8 @@ public class MappingElasticsearchConverter
|
||||
protected @Nullable Environment environment;
|
||||
|
||||
private final SpELContext spELContext = new SpELContext(new MapAccessor());
|
||||
private final SpelExpressionParser expressionParser = new SpelExpressionParser();
|
||||
private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory = new CachingValueExpressionEvaluatorFactory(
|
||||
expressionParser, this, spELContext);
|
||||
new SpelExpressionParser(), this, spELContext);
|
||||
|
||||
private final EntityInstantiators instantiators = new EntityInstantiators();
|
||||
private final ElasticsearchTypeMapper typeMapper;
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2026-present 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.data.elasticsearch.core.index;
|
||||
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.springframework.data.elasticsearch.annotations.CustomIndexOption;
|
||||
|
||||
/**
|
||||
* Writes the particular instance of the {@code CustomIndexOption} into the mapping, represented by objectNode.
|
||||
*
|
||||
* @author Andriy Redko
|
||||
* @since 6.2
|
||||
*/
|
||||
public interface IndexOptionMapper {
|
||||
void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode);
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright 2026-present 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.data.elasticsearch.core.index;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.BigIntegerNode;
|
||||
import tools.jackson.databind.node.BooleanNode;
|
||||
import tools.jackson.databind.node.DecimalNode;
|
||||
import tools.jackson.databind.node.NullNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
import tools.jackson.databind.node.StringNode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.data.elasticsearch.annotations.CustomIndexOption;
|
||||
|
||||
/**
|
||||
* The collection of predefined {@link IndexOptionMapper}s
|
||||
*
|
||||
* @author Andriy Redko
|
||||
* @since 6.2
|
||||
*/
|
||||
public final class IndexOptionMappers {
|
||||
private IndexOptionMappers() {}
|
||||
|
||||
/**
|
||||
* Writes the {@link CustomIndexOption} instance into mapping as JSON string (or array of strings) property
|
||||
*/
|
||||
public static final class StringMapper implements IndexOptionMapper {
|
||||
@Override
|
||||
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
|
||||
final String[] values = Objects.requireNonNull(indexOption.values(), "Values are required");
|
||||
|
||||
if (values.length == 1) {
|
||||
writePropertyAsString(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
|
||||
} else if (values.length > 1) {
|
||||
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
|
||||
} else {
|
||||
removeProperty(indexOption.name(), objectNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the {@link CustomIndexOption} instance into mapping as JSON number (or array of numbers) property
|
||||
*/
|
||||
public static final class NumberMapper implements IndexOptionMapper {
|
||||
@Override
|
||||
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
|
||||
final @Nullable Number[] values = toNumbers(Objects.requireNonNull(indexOption.values(), "Values are required"));
|
||||
|
||||
if (values.length == 1) {
|
||||
writePropertyAsNumber(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
|
||||
} else if (values.length > 1) {
|
||||
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
|
||||
} else {
|
||||
removeProperty(indexOption.name(), objectNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the {@link CustomIndexOption} instance into mapping as JSON boolean (or array of booleans) property
|
||||
*/
|
||||
public static final class BooleanMapper implements IndexOptionMapper {
|
||||
@Override
|
||||
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
|
||||
final @Nullable Boolean[] values = toBoolean(Objects.requireNonNull(indexOption.values(), "Values are required"));
|
||||
|
||||
if (values.length == 1) {
|
||||
writePropertyAsBoolean(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
|
||||
} else if (values.length > 1) {
|
||||
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
|
||||
} else {
|
||||
removeProperty(indexOption.name(), objectNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the array of strings to array of booleans.
|
||||
*
|
||||
* @param values array of strings
|
||||
* @return array of booleans
|
||||
*/
|
||||
private static @Nullable Boolean[] toBoolean(@Nullable String[] values) {
|
||||
return Arrays.stream(values).map(Boolean::valueOf).toArray(Boolean[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the array of strings to array of numbers, throwing {@link NumberFormatException} if the conversion is not
|
||||
* possible.
|
||||
*
|
||||
* @param values array of strings
|
||||
* @return array of numbers
|
||||
* @throws NumberFormatException
|
||||
*/
|
||||
private static @Nullable Number[] toNumbers(@Nullable String[] values) {
|
||||
final @Nullable Number[] numbers = new Number[values.length];
|
||||
|
||||
for (int j = 0; j < values.length; ++j) {
|
||||
Number number = null;
|
||||
var value = values[j];
|
||||
|
||||
if (value != null) {
|
||||
try {
|
||||
number = new BigInteger(value);
|
||||
} catch (NumberFormatException e) {
|
||||
number = new BigDecimal(value);
|
||||
}
|
||||
}
|
||||
|
||||
numbers[j] = number;
|
||||
}
|
||||
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a property as a JSON boolean value.
|
||||
*
|
||||
* @param name property name
|
||||
* @param override override if present
|
||||
* @param value property value
|
||||
* @param objectNode JSON object node
|
||||
*/
|
||||
private static void writePropertyAsBoolean(String name, boolean override, @Nullable Boolean value,
|
||||
ObjectNode objectNode) {
|
||||
final JsonNode node = BooleanNode.valueOf(value);
|
||||
|
||||
if (objectNode.has(name) && override) {
|
||||
objectNode.replace(name, node);
|
||||
} else {
|
||||
objectNode.putIfAbsent(name, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a property as a JSON number value.
|
||||
*
|
||||
* @param name property name
|
||||
* @param override override if present
|
||||
* @param value property value
|
||||
* @param objectNode JSON object node
|
||||
*/
|
||||
private static void writePropertyAsNumber(String name, boolean override, @Nullable Number value,
|
||||
ObjectNode objectNode) {
|
||||
final JsonNode node = toJsonNode(value);
|
||||
|
||||
if (objectNode.has(name) && override) {
|
||||
objectNode.replace(name, node);
|
||||
} else {
|
||||
objectNode.putIfAbsent(name, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a property as a JSON string value.
|
||||
*
|
||||
* @param name property name
|
||||
* @param override override if present
|
||||
* @param value property value
|
||||
* @param objectNode JSON object node
|
||||
*/
|
||||
private static void writePropertyAsString(String name, boolean override, String value, ObjectNode objectNode) {
|
||||
final StringNode node = StringNode.valueOf(value);
|
||||
|
||||
if (objectNode.has(name) && override) {
|
||||
objectNode.replace(name, node);
|
||||
} else {
|
||||
objectNode.putIfAbsent(name, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the property if present.
|
||||
*
|
||||
* @param name property name
|
||||
* @param objectNode JSON object node
|
||||
*/
|
||||
private static void removeProperty(String name, ObjectNode objectNode) {
|
||||
if (objectNode.has(name)) {
|
||||
objectNode.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a property as a JSON array of boolean values.
|
||||
*
|
||||
* @param name property name
|
||||
* @param override override if present
|
||||
* @param values property values
|
||||
* @param objectNode JSON object node
|
||||
*/
|
||||
private static void writePropertyAsArray(String name, boolean override, @Nullable Boolean[] values,
|
||||
ObjectNode objectNode) {
|
||||
writePropertyAsArray(
|
||||
name,
|
||||
override,
|
||||
objectNode
|
||||
.arrayNode()
|
||||
.addAll(
|
||||
Arrays.stream(values).map(b -> b != null ? BooleanNode.valueOf(b) : NullNode.getInstance()).toList()),
|
||||
objectNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a property as a JSON array of number values.
|
||||
*
|
||||
* @param name property name
|
||||
* @param override override if present
|
||||
* @param values property values
|
||||
* @param objectNode JSON object node
|
||||
*/
|
||||
private static void writePropertyAsArray(String name, boolean override, @Nullable Number[] values,
|
||||
ObjectNode objectNode) {
|
||||
writePropertyAsArray(
|
||||
name,
|
||||
override,
|
||||
objectNode
|
||||
.arrayNode()
|
||||
.addAll(Arrays.stream(values).map(IndexOptionMappers::toJsonNode).toList()),
|
||||
objectNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a property as a JSON array of string values.
|
||||
*
|
||||
* @param name property name
|
||||
* @param override override if present
|
||||
* @param values property values
|
||||
* @param objectNode JSON object node
|
||||
*/
|
||||
private static void writePropertyAsArray(String name, boolean override, String[] values, ObjectNode objectNode) {
|
||||
writePropertyAsArray(
|
||||
name,
|
||||
override,
|
||||
objectNode
|
||||
.arrayNode()
|
||||
.addAll(Arrays.stream(values).map(StringNode::valueOf).toList()),
|
||||
objectNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a property as a JSON array.
|
||||
*
|
||||
* @param name property name
|
||||
* @param override override if present
|
||||
* @param arrayNode JSON array
|
||||
* @param objectNode JSON object node
|
||||
*/
|
||||
private static void writePropertyAsArray(String name, boolean override, ArrayNode arrayNode, ObjectNode objectNode) {
|
||||
if (objectNode.has(name) && override) {
|
||||
objectNode.replace(name, arrayNode);
|
||||
} else {
|
||||
objectNode.putIfAbsent(name, arrayNode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number into appropriate JSON node. Only {@link DecimalNode} and {@link BigInteger} types are supported.
|
||||
*
|
||||
* @param value {@link Number} instance to convert
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
private static JsonNode toJsonNode(@Nullable Number value) {
|
||||
|
||||
if (value == null) {
|
||||
return NullNode.getInstance();
|
||||
}
|
||||
|
||||
if (value instanceof BigDecimal d) {
|
||||
return DecimalNode.valueOf(d);
|
||||
} else if (value instanceof BigInteger i) {
|
||||
return BigIntegerNode.valueOf(i);
|
||||
} else {
|
||||
// We only support BigDecimal or BigInteger numeric values, should never happen since the
|
||||
// conversion is constrained within this class only.
|
||||
throw new IllegalArgumentException("Only BigDecimal or BigInteger numbers are supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -27,6 +27,7 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.elasticsearch.annotations.*;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -120,6 +121,7 @@ public class MappingParameters {
|
||||
private final TermVector termVector;
|
||||
private final FieldType type;
|
||||
private final String mappedTypeName;
|
||||
private final CustomIndexOption[] customIndexOptions;
|
||||
|
||||
/**
|
||||
* extracts the mapping parameters from the relevant annotations.
|
||||
@@ -185,6 +187,7 @@ public class MappingParameters {
|
||||
Assert.isTrue(field.enabled() || type == FieldType.Object, "enabled false is only allowed for field type object");
|
||||
enabled = field.enabled();
|
||||
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
|
||||
customIndexOptions = field.customIndexOptions();
|
||||
}
|
||||
|
||||
protected MappingParameters(InnerField field) {
|
||||
@@ -231,6 +234,7 @@ public class MappingParameters {
|
||||
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
|
||||
enabled = true;
|
||||
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
|
||||
customIndexOptions = field.customIndexOptions();
|
||||
}
|
||||
|
||||
public boolean isStore() {
|
||||
@@ -419,6 +423,12 @@ public class MappingParameters {
|
||||
if (eagerGlobalOrdinals) {
|
||||
objectNode.put(FIELD_PARAM_EAGER_GLOBAL_ORDINALS, eagerGlobalOrdinals);
|
||||
}
|
||||
|
||||
// At last, check the custom index options
|
||||
for (CustomIndexOption customIndexOption: customIndexOptions) {
|
||||
final IndexOptionMapper mapper = BeanUtils.instantiateClass(customIndexOption.mapper());
|
||||
mapper.writeIndexOptionTo(customIndexOption, objectNode);
|
||||
}
|
||||
}
|
||||
|
||||
protected String analyzer() {
|
||||
@@ -548,4 +558,8 @@ public class MappingParameters {
|
||||
protected String mappedTypeName() {
|
||||
return mappedTypeName;
|
||||
}
|
||||
|
||||
protected CustomIndexOption[] customIndexOptions() {
|
||||
return customIndexOptions;
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Dynamic;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
|
||||
import org.springframework.data.elasticsearch.core.index.Settings;
|
||||
import org.springframework.data.elasticsearch.core.join.JoinField;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
@@ -39,6 +40,7 @@ import org.springframework.data.mapping.model.FieldNamingStrategy;
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Subhobrata Dey
|
||||
* @author Steven Pearce
|
||||
*/
|
||||
public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, ElasticsearchPersistentProperty> {
|
||||
|
||||
@@ -179,6 +181,12 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
|
||||
*/
|
||||
boolean writeTypeHints();
|
||||
|
||||
/**
|
||||
* @return the {@link ElasticsearchServerType} for the server that stores this entity
|
||||
* @since 6.2
|
||||
*/
|
||||
ElasticsearchServerType getServerType();
|
||||
|
||||
/**
|
||||
* @return the {@code dynamic} mapping parameter value.
|
||||
* @since 4.3
|
||||
|
||||
+16
-1
@@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core.mapping;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.data.core.TypeInformation;
|
||||
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
|
||||
import org.springframework.data.mapping.context.AbstractMappingContext;
|
||||
import org.springframework.data.mapping.model.FieldNamingStrategy;
|
||||
import org.springframework.data.mapping.model.Property;
|
||||
@@ -31,6 +32,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
* @author Mohsin Husen
|
||||
* @author Mark Paluch
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Steven Pearce
|
||||
*/
|
||||
public class SimpleElasticsearchMappingContext
|
||||
extends AbstractMappingContext<SimpleElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> {
|
||||
@@ -39,6 +41,7 @@ public class SimpleElasticsearchMappingContext
|
||||
|
||||
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
|
||||
private boolean writeTypeHints = true;
|
||||
private ElasticsearchServerType serverType = ElasticsearchServerType.DEFAULT;
|
||||
|
||||
/**
|
||||
* Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied.
|
||||
@@ -61,6 +64,18 @@ public class SimpleElasticsearchMappingContext
|
||||
this.writeTypeHints = writeTypeHints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ElasticSearch Server Type
|
||||
*
|
||||
* @param serverType the {@link ElasticsearchServerType} to be used to set the server Type. The Default value will
|
||||
* support the standard configurations, and SERVERLESS will support ElasticSearch Serverless
|
||||
*
|
||||
* @since 6.2
|
||||
*/
|
||||
public void setServerType(@Nullable ElasticsearchServerType serverType) {
|
||||
this.serverType = serverType == null ? ElasticsearchServerType.DEFAULT : serverType;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldCreatePersistentEntityFor(TypeInformation<?> type) {
|
||||
return !ElasticsearchSimpleTypes.HOLDER.isSimpleType(type.getType());
|
||||
@@ -69,7 +84,7 @@ public class SimpleElasticsearchMappingContext
|
||||
@Override
|
||||
protected <T> SimpleElasticsearchPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
|
||||
return new SimpleElasticsearchPersistentEntity<>(typeInformation,
|
||||
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints));
|
||||
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints, serverType));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+101
-58
@@ -33,24 +33,26 @@ import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.Routing;
|
||||
import org.springframework.data.elasticsearch.annotations.Setting;
|
||||
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
|
||||
import org.springframework.data.elasticsearch.core.index.Settings;
|
||||
import org.springframework.data.elasticsearch.core.join.JoinField;
|
||||
import org.springframework.data.elasticsearch.core.query.Query;
|
||||
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||
import org.springframework.data.elasticsearch.utils.spel.ExpressionUtils;
|
||||
import org.springframework.data.expression.ValueEvaluationContext;
|
||||
import org.springframework.data.expression.ValueExpression;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
||||
import org.springframework.data.mapping.model.FieldNamingStrategy;
|
||||
import org.springframework.data.spel.ExpressionDependencies;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionException;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Elasticsearch specific {@link org.springframework.data.mapping.PersistentEntity} implementation holding
|
||||
@@ -64,15 +66,15 @@ import org.springframework.util.Assert;
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Subhobrata Dey
|
||||
* @author Steven Pearce
|
||||
*/
|
||||
public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntity<T, ElasticsearchPersistentProperty>
|
||||
implements ElasticsearchPersistentEntity<T> {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(SimpleElasticsearchPersistentEntity.class);
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
|
||||
private @Nullable final Document document;
|
||||
private @Nullable String indexName;
|
||||
private final String unresolvedIndexName;
|
||||
private final Lazy<SettingsParameter> settingsParameter;
|
||||
private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty;
|
||||
private @Nullable ElasticsearchPersistentProperty joinFieldProperty;
|
||||
@@ -82,14 +84,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
private final boolean alwaysWriteMapping;
|
||||
private final Dynamic dynamic;
|
||||
private final Map<String, ElasticsearchPersistentProperty> fieldNamePropertyCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Expression> routingExpressions = new ConcurrentHashMap<>();
|
||||
private @Nullable String routing;
|
||||
private final ContextConfiguration contextConfiguration;
|
||||
private final Set<Alias> aliases = new HashSet<>();
|
||||
|
||||
private final ConcurrentHashMap<String, Expression> indexNameExpressions = new ConcurrentHashMap<>();
|
||||
private final Lazy<EvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
|
||||
|
||||
private final boolean storeIdInSource;
|
||||
private final boolean storeVersionInSource;
|
||||
|
||||
@@ -108,10 +105,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
this.settingsParameter = Lazy.of(() -> buildSettingsParameter(clazz));
|
||||
|
||||
if (document != null) {
|
||||
|
||||
Assert.hasText(document.indexName(),
|
||||
" Unknown indexName. Make sure the indexName is defined. e.g @Document(indexName=\"foo\")");
|
||||
this.indexName = document.indexName();
|
||||
|
||||
this.unresolvedIndexName = document.indexName();
|
||||
this.versionType = document.versionType();
|
||||
this.createIndexAndMapping = document.createIndex();
|
||||
this.alwaysWriteMapping = document.alwaysWriteMapping();
|
||||
@@ -120,6 +117,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
this.storeVersionInSource = document.storeVersionInSource();
|
||||
buildAliases();
|
||||
} else {
|
||||
this.unresolvedIndexName = getTypeInformation().getType().getSimpleName();
|
||||
this.dynamic = Dynamic.INHERIT;
|
||||
this.storeIdInSource = true;
|
||||
this.storeVersionInSource = true;
|
||||
@@ -136,13 +134,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
}
|
||||
}
|
||||
|
||||
private String getIndexName() {
|
||||
return indexName != null ? indexName : getTypeInformation().getType().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IndexCoordinates getIndexCoordinates() {
|
||||
return resolve(IndexCoordinates.of(getIndexName()));
|
||||
return resolve(IndexCoordinates.of(unresolvedIndexName));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -174,7 +168,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
@Nullable
|
||||
@Override
|
||||
public String getRefreshInterval() {
|
||||
return settingsParameter.get().refreshIntervall;
|
||||
return settingsParameter.get().refreshInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -196,6 +190,11 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
return contextConfiguration.getFieldNamingStrategy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchServerType getServerType() {
|
||||
return contextConfiguration.getServerType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTypeHints() {
|
||||
|
||||
@@ -368,52 +367,55 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
|
||||
Assert.notNull(name, "name must not be null");
|
||||
|
||||
Expression expression = getExpressionForIndexName(name);
|
||||
ValueExpression expression = ExpressionUtils.detectExpression(name);
|
||||
|
||||
String resolvedName = expression != null ? expression.getValue(indexNameEvaluationContext.get(), String.class)
|
||||
: null;
|
||||
return resolvedName != null ? resolvedName : name;
|
||||
Object resolvedName;
|
||||
if (expression != null && !expression.isLiteral()) {
|
||||
var valueEvaluationContext = getValueEvaluationContext(name);
|
||||
resolvedName = expression.evaluate(valueEvaluationContext);
|
||||
} else {
|
||||
resolvedName = null;
|
||||
}
|
||||
return resolvedName != null ? ObjectUtils.nullSafeToString(resolvedName) : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an {@link Expression} for #name if name contains a {@link ParserContext#TEMPLATE_EXPRESSION} otherwise
|
||||
* returns {@literal null}.
|
||||
* build the {@link ValueEvaluationContext} from the expression.
|
||||
*
|
||||
* @param name the name to get the expression for
|
||||
* @return Expression may be null
|
||||
* @return ValueEvaluationContext
|
||||
* @param expression
|
||||
*/
|
||||
@Nullable
|
||||
private Expression getExpressionForIndexName(String name) {
|
||||
return indexNameExpressions.computeIfAbsent(name, s -> {
|
||||
Expression expr = PARSER.parseExpression(s, ParserContext.TEMPLATE_EXPRESSION);
|
||||
return expr instanceof LiteralExpression ? null : expr;
|
||||
});
|
||||
private ValueEvaluationContext getValueEvaluationContext(@Nullable String expression) {
|
||||
|
||||
ValueExpression valueExpression = ExpressionUtils.detectExpression(expression);
|
||||
|
||||
return getValueEvaluationContext(valueExpression);
|
||||
}
|
||||
|
||||
/**
|
||||
* build the {@link EvaluationContext} considering {@link ExpressionDependencies} from the name returned by
|
||||
* {@link #getIndexName()}.
|
||||
* build the {@link ValueEvaluationContext} considering {@link ExpressionDependencies} from the expression.
|
||||
*
|
||||
* @return EvaluationContext
|
||||
* @return ValueEvaluationContext
|
||||
* @param expression
|
||||
*/
|
||||
private EvaluationContext getIndexNameEvaluationContext() {
|
||||
private ValueEvaluationContext getValueEvaluationContext(@Nullable ValueExpression expression) {
|
||||
|
||||
Expression expression = getExpressionForIndexName(getIndexName());
|
||||
ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression)
|
||||
: ExpressionDependencies.none();
|
||||
var expressionDependencies = expression != null ? expression.getExpressionDependencies() : null;
|
||||
|
||||
// noinspection ConstantConditions
|
||||
return getEvaluationContext(null, expressionDependencies);
|
||||
return expressionDependencies != null ? getValueEvaluationContext(null, expressionDependencies)
|
||||
: getValueEvaluationContext((Object) null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String resolveRouting(T bean) {
|
||||
|
||||
// we need a @Routing annotation on the entity
|
||||
if (routing == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if there is a property with name of of the @Routing annotation value, return the property's value
|
||||
ElasticsearchPersistentProperty persistentProperty = getPersistentProperty(routing);
|
||||
|
||||
if (persistentProperty != null) {
|
||||
@@ -422,14 +424,29 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
return propertyValue != null ? propertyValue.toString() : null;
|
||||
}
|
||||
|
||||
// try to resolve the annotations value
|
||||
try {
|
||||
Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression);
|
||||
ExpressionDependencies expressionDependencies = ExpressionDependencies.discover(expression);
|
||||
var routingValue = routing;
|
||||
ValueExpression expression = ExpressionUtils.detectExpression(routingValue);
|
||||
|
||||
EvaluationContext context = getEvaluationContext(null, expressionDependencies);
|
||||
context.setVariable("entity", bean);
|
||||
if (expression == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expression.getValue(context, String.class);
|
||||
if (expression.isLiteral()) {
|
||||
// before using ValueExpressions, we had SpEL expressions without the "#{...},
|
||||
// so try if we get a valid expression after wrapping that
|
||||
routingValue = "#{" + routing + "}";
|
||||
expression = ExpressionUtils.detectExpression(routingValue);
|
||||
|
||||
if (expression == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ValueEvaluationContext context = getValueEvaluationContext(routingValue);
|
||||
context.getEvaluationContext().setVariable("entity", bean);
|
||||
return ObjectUtils.nullSafeToString(expression.evaluate(context));
|
||||
} catch (EvaluationException e) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
"Could not resolve expression: " + routing + " for object of class " + bean.getClass().getCanonicalName(), e);
|
||||
@@ -447,9 +464,13 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
}
|
||||
|
||||
try {
|
||||
Expression expression = PARSER.parseExpression(settingPathFromParameter, ParserContext.TEMPLATE_EXPRESSION);
|
||||
return (expression instanceof LiteralExpression) ? settingPathFromParameter
|
||||
: expression.getValue(getEvaluationContext(null, ExpressionDependencies.discover(expression)), String.class);
|
||||
ValueExpression expression = ExpressionUtils.detectExpression(settingPathFromParameter);
|
||||
|
||||
if (expression == null || expression instanceof LiteralExpression) {
|
||||
return settingPathFromParameter;
|
||||
}
|
||||
|
||||
return ObjectUtils.nullSafeToString(expression.evaluate(getValueEvaluationContext(settingPathFromParameter)));
|
||||
} catch (ExpressionException e) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
"Could not resolve expression: " + settingPathFromParameter + " for @Setting.settingPath ", e);
|
||||
@@ -468,9 +489,13 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
|
||||
// default values
|
||||
settingsParameter.useServerConfiguration = false;
|
||||
settingsParameter.serverType = contextConfiguration.serverType;
|
||||
settingsParameter.shards = 1;
|
||||
settingsParameter.replicas = 1;
|
||||
settingsParameter.refreshIntervall = "1s";
|
||||
settingsParameter.refreshInterval = switch (contextConfiguration.serverType) {
|
||||
case DEFAULT -> "1s";
|
||||
case SERVERLESS -> "5s";
|
||||
};
|
||||
|
||||
if (settingAnnotation != null) {
|
||||
processSettingAnnotation(settingAnnotation, settingsParameter);
|
||||
@@ -484,7 +509,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
settingsParameter.settingPath = settingAnnotation.settingPath();
|
||||
settingsParameter.shards = settingAnnotation.shards();
|
||||
settingsParameter.replicas = settingAnnotation.replicas();
|
||||
settingsParameter.refreshIntervall = settingAnnotation.refreshInterval();
|
||||
if (StringUtils.hasText(settingAnnotation.refreshInterval())) {
|
||||
settingsParameter.refreshInterval = settingAnnotation.refreshInterval();
|
||||
}
|
||||
settingsParameter.indexStoreType = settingAnnotation.indexStoreType();
|
||||
|
||||
String[] sortFields = settingAnnotation.sortFields();
|
||||
@@ -560,10 +587,11 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
*/
|
||||
private static class SettingsParameter {
|
||||
boolean useServerConfiguration = false;
|
||||
ElasticsearchServerType serverType = ElasticsearchServerType.DEFAULT;
|
||||
@Nullable String settingPath;
|
||||
short shards;
|
||||
short replicas;
|
||||
@Nullable String refreshIntervall;
|
||||
@Nullable String refreshInterval;
|
||||
@Nullable String indexStoreType;
|
||||
private String @Nullable [] sortFields;
|
||||
private Setting.@Nullable SortOrder @Nullable [] sortOrders;
|
||||
@@ -576,12 +604,16 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
return new Settings();
|
||||
}
|
||||
|
||||
var index = new Settings() //
|
||||
.append("number_of_shards", String.valueOf(shards)) //
|
||||
.append("number_of_replicas", String.valueOf(replicas));
|
||||
var index = new Settings();
|
||||
|
||||
if (refreshIntervall != null) {
|
||||
index.append("refresh_interval", refreshIntervall);
|
||||
if (ElasticsearchServerType.DEFAULT.equals(serverType)) {
|
||||
index
|
||||
.append("number_of_shards", String.valueOf(shards))
|
||||
.append("number_of_replicas", String.valueOf(replicas));
|
||||
}
|
||||
|
||||
if (refreshInterval != null) {
|
||||
index.append("refresh_interval", refreshInterval);
|
||||
}
|
||||
|
||||
if (indexStoreType != null && !"fs".equals(indexStoreType)) {
|
||||
@@ -619,10 +651,17 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
|
||||
private final FieldNamingStrategy fieldNamingStrategy;
|
||||
private final boolean writeTypeHints;
|
||||
private final ElasticsearchServerType serverType;
|
||||
|
||||
ContextConfiguration(FieldNamingStrategy fieldNamingStrategy, boolean writeTypeHints) {
|
||||
this(fieldNamingStrategy, writeTypeHints, ElasticsearchServerType.DEFAULT);
|
||||
}
|
||||
|
||||
ContextConfiguration(FieldNamingStrategy fieldNamingStrategy, boolean writeTypeHints,
|
||||
ElasticsearchServerType serverType) {
|
||||
this.fieldNamingStrategy = fieldNamingStrategy;
|
||||
this.writeTypeHints = writeTypeHints;
|
||||
this.serverType = serverType;
|
||||
}
|
||||
|
||||
public FieldNamingStrategy getFieldNamingStrategy() {
|
||||
@@ -632,6 +671,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
public boolean getWriteTypeHints() {
|
||||
return writeTypeHints;
|
||||
}
|
||||
|
||||
public ElasticsearchServerType getServerType() {
|
||||
return serverType;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* A repository query that is built from the the method name in the repository definition. Was originally named
|
||||
* A repository query that is built from the method name in the repository definition. Was originally named
|
||||
* ElasticsearchPartQuery.
|
||||
*
|
||||
* @author Rizwan Idrees
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2021-present 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.data.elasticsearch.utils.spel;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.data.expression.ValueEvaluationContext;
|
||||
import org.springframework.data.expression.ValueExpression;
|
||||
import org.springframework.data.expression.ValueExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.util.ConcurrentLruCache;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Internal utility class for dealing with {@link ValueExpression} and potential ones. Shamelessly copied from Spring
|
||||
* Data MongoDB. Adapted afterwards to our needs.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @since 6.2
|
||||
*/
|
||||
public final class ExpressionUtils {
|
||||
|
||||
private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
|
||||
|
||||
public static final int CAPACITY = 256;
|
||||
private static final ConcurrentLruCache<String, ValueExpression> expressionCache = new ConcurrentLruCache<>(
|
||||
CAPACITY, PARSER::parse);
|
||||
|
||||
/**
|
||||
* Returns a SpEL {@link ValueExpression} if the given {@link String} is not empty. ValueExpressions are stored in a
|
||||
* cash.
|
||||
*
|
||||
* @param potentialExpression can be {@literal null}
|
||||
* @return {@link ValueExpression} or null when input is empty
|
||||
*/
|
||||
@Contract("null -> null")
|
||||
public static @Nullable ValueExpression detectExpression(@Nullable String potentialExpression) {
|
||||
|
||||
if (!StringUtils.hasText(potentialExpression)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expressionCache.get(potentialExpression);
|
||||
}
|
||||
|
||||
/**
|
||||
* evbaluates value against the context provided by valueEvaluationContextSupplier. If value cannot be transformed
|
||||
* into a {@link ValueExpression}, the defaultValue is returned.
|
||||
*
|
||||
* @param value the value to evaluate
|
||||
* @param valueEvaluationContextSupplier supplies the context to use for evaluation
|
||||
* @param defaultValue
|
||||
* @return
|
||||
*/
|
||||
public static @Nullable Object evaluate(String value, Supplier<ValueEvaluationContext> valueEvaluationContextSupplier,
|
||||
@Nullable Object defaultValue) {
|
||||
|
||||
ValueExpression expression = detectExpression(value);
|
||||
|
||||
if (expression == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
var evaluated = expression.evaluate(valueEvaluationContextSupplier.get());
|
||||
return evaluated == null ? defaultValue : evaluated;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
@@ -178,7 +179,7 @@
|
||||
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 "[]"
|
||||
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
|
||||
@@ -186,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
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.
|
||||
@@ -199,18 +200,3 @@
|
||||
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.
|
||||
|
||||
=======================================================================
|
||||
|
||||
To the extent any open source subcomponents are licensed under the EPL and/or other
|
||||
similar licenses that require the source code and/or modifications to
|
||||
source code to be made available (as would be noted above), you may obtain a
|
||||
copy of the source code corresponding to the binaries for such open source
|
||||
components and modifications thereto, if any, (the "Source Files"), by
|
||||
downloading the Source Files from https://www.springsource.org/download,
|
||||
or by sending a request, with your name and address to: VMware, Inc., 3401 Hillview
|
||||
Avenue, Palo Alto, CA 94304, United States of America or email info@vmware.com. All
|
||||
such requests should clearly specify: OPEN SOURCE FILES REQUEST, Attention General
|
||||
Counsel. VMware shall mail a copy of the Source Files to you on a CD or equivalent
|
||||
physical medium. This offer to obtain a copy of the Source Files is valid for three
|
||||
years from the date you acquired this Software product.
|
||||
@@ -1,5 +1,5 @@
|
||||
Spring Data Elasticsearch 6.1 GA (2026.0.0)
|
||||
Copyright (c) [2013-2022] Pivotal Software, Inc.
|
||||
Spring Data Elasticsearch 6.2.0-M1 (2026.1.0)
|
||||
Copyright (c) [2013-2026] Pivotal Software, Inc.
|
||||
|
||||
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
You may not use this product except in compliance with the License.
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package org.springframework.data.elasticsearch.client;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
import static io.specto.hoverfly.junit.dsl.HoverflyDsl.*;
|
||||
import static io.specto.hoverfly.junit.verification.HoverflyVerifications.*;
|
||||
@@ -32,6 +31,7 @@ import io.specto.hoverfly.junit5.api.HoverflyConfig;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -108,6 +108,8 @@ public class RestClientsTest {
|
||||
AtomicInteger connectionConfigurerCount = new AtomicInteger(0);
|
||||
AtomicInteger connectionManagerConfigurerCount = new AtomicInteger(0);
|
||||
AtomicInteger requestConfigurerCount = new AtomicInteger(0);
|
||||
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> null);
|
||||
AtomicReference<String> supplierThreadName = new AtomicReference<>();
|
||||
|
||||
ClientConfigurationBuilder configurationBuilder = new ClientConfigurationBuilder();
|
||||
configurationBuilder //
|
||||
@@ -115,7 +117,9 @@ public class RestClientsTest {
|
||||
.withBasicAuth("user", "password") //
|
||||
.withDefaultHeaders(defaultHeaders) //
|
||||
.withHeaders(() -> {
|
||||
supplierThreadName.set(Thread.currentThread().getName());
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.add("thread", threadLocal.get());
|
||||
httpHeaders.add("supplied", "val0");
|
||||
httpHeaders.add("supplied", "val" + supplierCount.getAndIncrement());
|
||||
return httpHeaders;
|
||||
@@ -179,6 +183,7 @@ public class RestClientsTest {
|
||||
// do several calls to check that the headerSupplier provided values are set
|
||||
int startValue = clientUnderTest.usesInitialRequest() ? 2 : 1;
|
||||
for (int i = startValue; i <= startValue + 2; i++) {
|
||||
threadLocal.set("local");
|
||||
clientUnderTest.ping();
|
||||
|
||||
verify(headRequestedFor(urlEqualTo("/")) //
|
||||
@@ -189,10 +194,15 @@ public class RestClientsTest {
|
||||
.withHeader("supplied", new EqualToPattern("val0")) //
|
||||
// on the first call Elasticsearch does the version check and thus already increments the counter
|
||||
.withHeader("supplied", new EqualToPattern("val" + i)) //
|
||||
.withHeader("supplied", including("val0", "val" + i)));
|
||||
.withHeader("supplied", including("val0", "val" + i)) //
|
||||
.withHeader("thread", new EqualToPattern("local")));
|
||||
;
|
||||
}
|
||||
|
||||
// the headers supplier must run on the calling thread, so a ThreadLocal set above is visible (#3300).
|
||||
// this guards against future regressions that move the supplier back onto the async I/O thread.
|
||||
assertThat(supplierThreadName.get()).isEqualTo(Thread.currentThread().getName());
|
||||
|
||||
assertThat(restClientConfigurerCount).hasValue(clientUnderTestFactory.getExpectedRestClientConfigurerCalls());
|
||||
assertThat(httpClientConfigurerCount).hasValue(1);
|
||||
assertThat(connectionConfigurerCount).hasValue(clientUnderTestFactory.getExpectedConnectionConfigurerCalls());
|
||||
|
||||
+44
@@ -19,6 +19,7 @@ import co.elastic.clients.elasticsearch.core.search.Hit;
|
||||
import co.elastic.clients.json.JsonData;
|
||||
import co.elastic.clients.json.JsonpMapper;
|
||||
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
|
||||
import jakarta.json.JsonValue;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -77,6 +78,49 @@ class DocumentAdaptersUnitTests {
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
@Test // #3178
|
||||
@DisplayName("should adapt parser backed search Hit fields to Java types")
|
||||
void shouldAdaptParserBackedSearchHitFieldsToJavaTypes() {
|
||||
|
||||
Hit<EntityAsMap> searchHit = new Hit.Builder<EntityAsMap>() //
|
||||
.index("index") //
|
||||
.id("my-id") //
|
||||
.fields("objectField", JsonData.fromJson(
|
||||
"{\"string\":\"value\",\"integer\":2,\"decimal\":1.5,\"nested\":{\"flag\":true}}")) //
|
||||
.fields("listField", JsonData.fromJson("[\"listValue\",{\"nested\":3},true,null]")) //
|
||||
.fields("stringField", JsonData.fromJson("\"stringValue\"")) //
|
||||
.build(); //
|
||||
|
||||
SearchDocument document = DocumentAdapters.from(searchHit, jsonpMapper);
|
||||
|
||||
SoftAssertions softly = new SoftAssertions();
|
||||
|
||||
Object objectFieldValue = document.get("objectField");
|
||||
Object listFieldValue = document.get("listField");
|
||||
Object objectFieldFirstValue = document.getFieldValue("objectField");
|
||||
Object listFieldFirstValue = document.getFieldValue("listField");
|
||||
|
||||
softly.assertThat(objectFieldValue).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
|
||||
softly.assertThat(listFieldValue).isInstanceOf(List.class).isNotInstanceOf(JsonValue.class);
|
||||
softly.assertThat(document.get("stringField")).isEqualTo("stringValue");
|
||||
softly.assertThat(objectFieldFirstValue).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
|
||||
softly.assertThat(listFieldFirstValue).isEqualTo("listValue");
|
||||
|
||||
// noinspection unchecked
|
||||
Map<String, Object> objectField = (Map<String, Object>) objectFieldValue;
|
||||
softly.assertThat(objectField.get("string")).isEqualTo("value");
|
||||
softly.assertThat(objectField.get("integer")).isEqualTo(2);
|
||||
softly.assertThat(objectField.get("decimal")).isEqualTo(1.5d);
|
||||
softly.assertThat(objectField.get("nested")).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
|
||||
|
||||
// noinspection unchecked
|
||||
List<Object> listField = (List<Object>) listFieldValue;
|
||||
softly.assertThat(listField).containsExactly("listValue", Collections.singletonMap("nested", 3), true, null);
|
||||
softly.assertThat(listField.get(1)).isNotInstanceOf(JsonValue.class);
|
||||
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
@Test // #1973
|
||||
@DisplayName("should adapt search Hit from source")
|
||||
void shouldAdaptSearchHitFromSource() {
|
||||
|
||||
+40
@@ -41,6 +41,9 @@ import org.springframework.data.elasticsearch.annotations.*;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.core.IndexOperations;
|
||||
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
|
||||
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.BooleanMapper;
|
||||
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.NumberMapper;
|
||||
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.StringMapper;
|
||||
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
@@ -273,6 +276,12 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
|
||||
void shouldWriteCorrectMappingForDenseVectorProperty() {
|
||||
operations.indexOps(SimilarityEntity.class).createWithMapping();
|
||||
}
|
||||
|
||||
@Test // #3299
|
||||
@DisplayName("should write correct mapping for dense vector property with custom index options")
|
||||
void shouldWriteCorrectMappingForDenseVectorPropertyWithCustomIndexOptions() {
|
||||
operations.indexOps(SimilarityCustomIndexOptionEntity.class).createWithMapping();
|
||||
}
|
||||
|
||||
@Test // #2845
|
||||
@DisplayName("should write mapping with field aliases")
|
||||
@@ -280,6 +289,12 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
|
||||
operations.indexOps(FieldAliasEntity.class).createWithMapping();
|
||||
}
|
||||
|
||||
@Test // #3298
|
||||
@DisplayName("should write index prefixes with default value")
|
||||
void shouldWriteIndexPrefixesWithDefaultValue() {
|
||||
operations.indexOps(TextIndexPrefixesEntity.class).createWithMapping();
|
||||
}
|
||||
|
||||
// region Entities
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||
static class Book {
|
||||
@@ -908,6 +923,22 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
|
||||
@Field(name = "dotted.field", type = Text) private String dottedField;
|
||||
}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||
static class SimilarityCustomIndexOptionEntity {
|
||||
@Nullable
|
||||
@Id private String id;
|
||||
|
||||
@Field(type = FieldType.Dense_Vector, dims = 42,
|
||||
knnSimilarity = KnnSimilarity.COSINE,
|
||||
customIndexOptions = {
|
||||
@CustomIndexOption(name = "similarity", values = "l2_norm", overrideIfPresent = true, mapper = StringMapper.class),
|
||||
@CustomIndexOption(name = "element_type", values = "bit", overrideIfPresent = false, mapper = StringMapper.class),
|
||||
@CustomIndexOption(name = "dims", values = "64", overrideIfPresent = true, mapper = NumberMapper.class),
|
||||
@CustomIndexOption(name = "index", values = "true", overrideIfPresent = true, mapper = BooleanMapper.class)
|
||||
}) private double @Nullable [] denseVector;
|
||||
}
|
||||
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||
static class SimilarityEntity {
|
||||
@Nullable
|
||||
@@ -931,5 +962,14 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
|
||||
@Field(type = Text) private String otherText;
|
||||
}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||
private static class TextIndexPrefixesEntity {
|
||||
@Id
|
||||
@Nullable private String id;
|
||||
@Nullable
|
||||
// the min value is set to the default
|
||||
@Field(type = Text, indexPrefixes = @IndexPrefixes(maxChars = 10)) private String someText;
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
+73
@@ -41,6 +41,9 @@ import org.springframework.data.domain.Range;
|
||||
import org.springframework.data.elasticsearch.annotations.*;
|
||||
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
|
||||
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
|
||||
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.BooleanMapper;
|
||||
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.NumberMapper;
|
||||
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.StringMapper;
|
||||
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
import org.springframework.data.elasticsearch.core.suggest.Completion;
|
||||
@@ -1371,6 +1374,37 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
|
||||
assertEquals(expected, mapping, false);
|
||||
}
|
||||
|
||||
|
||||
@Test // #3299
|
||||
@DisplayName("should write dense_vector properties for knn search with custom index options")
|
||||
void shouldWriteDenseVectorPropertiesWithKnnSearchCustomIndexOptions() throws JSONException {
|
||||
String expected = """
|
||||
{
|
||||
"properties":{
|
||||
"my_vector":{
|
||||
"dims":32,
|
||||
"element_type":"bit",
|
||||
"similarity":"dot_product",
|
||||
"index": true,
|
||||
"floats": [0.5644, 0.4445, 0.95442],
|
||||
"ints": [-1, 0, 1],
|
||||
"strings": ["s1", "", "s2"],
|
||||
"bools": [true, false],
|
||||
"index_options":{
|
||||
"type":"hnsw",
|
||||
"m":16,
|
||||
"ef_construction":100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String mapping = getMappingBuilder().buildPropertyMapping(DenseVectorEntityWithKnnSearchCustomIndexOptions.class);
|
||||
|
||||
assertEquals(expected, mapping, false);
|
||||
}
|
||||
// region entities
|
||||
|
||||
@Document(indexName = "ignore-above-index")
|
||||
@@ -2248,6 +2282,45 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class DenseVectorEntityWithKnnSearchCustomIndexOptions {
|
||||
@Nullable
|
||||
@Id private String id;
|
||||
|
||||
@Field(type = FieldType.Dense_Vector, dims = 16,
|
||||
knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, efConstruction = 100),
|
||||
knnSimilarity = KnnSimilarity.DOT_PRODUCT,
|
||||
customIndexOptions = {
|
||||
@CustomIndexOption(name = "similarity", values = "l2_norm", mapper = StringMapper.class),
|
||||
@CustomIndexOption(name = "element_type", values = "bit", mapper = StringMapper.class),
|
||||
@CustomIndexOption(name = "dims", values = "32", overrideIfPresent = true, mapper = NumberMapper.class),
|
||||
@CustomIndexOption(name = "index", values = "true", mapper = BooleanMapper.class),
|
||||
@CustomIndexOption(name = "floats", values = { "0.5644", "0.4445", "0.95442" }, mapper = NumberMapper.class),
|
||||
@CustomIndexOption(name = "ints", values = { "-1", "0", "1" }, mapper = NumberMapper.class),
|
||||
@CustomIndexOption(name = "strings", values = { "s1", "", "s2" }, mapper = StringMapper.class),
|
||||
@CustomIndexOption(name = "bools", values = { "false", "true" }, mapper = BooleanMapper.class),
|
||||
@CustomIndexOption(name = "type", mapper = StringMapper.class)
|
||||
}
|
||||
) private float @Nullable [] my_vector;
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public float @Nullable [] getMy_vector() {
|
||||
return my_vector;
|
||||
}
|
||||
|
||||
public void setMy_vector(float @Nullable [] my_vector) {
|
||||
this.my_vector = my_vector;
|
||||
}
|
||||
}
|
||||
|
||||
@Mapping(enabled = false)
|
||||
static class DisabledMappingEntity {
|
||||
@Nullable
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2024-present 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.data.elasticsearch.core.mapping;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
|
||||
public class SimpleElasticsearchPersistentEntityELCIntegrationTests extends SimpleElasticsearchPersistentEntityIntegrationTests {
|
||||
@Configuration
|
||||
static class Config extends ElasticsearchTemplateConfiguration {
|
||||
|
||||
@Bean
|
||||
IndexNameProvider indexNameProvider() {
|
||||
return new IndexNameProvider("persistent-entity");
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2021-present 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.data.elasticsearch.core.mapping;
|
||||
/*
|
||||
* Copyright 2024-present 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.
|
||||
*/
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
|
||||
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
|
||||
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
/**
|
||||
* @author Peter-Josef Meisch
|
||||
*/
|
||||
@SpringIntegrationTest
|
||||
@TestPropertySource(properties = { "entity.indexName = index-property" })
|
||||
public abstract class SimpleElasticsearchPersistentEntityIntegrationTests {
|
||||
|
||||
@Autowired private ElasticsearchOperations operations;
|
||||
@Autowired private IndexNameProvider indexNameProvider;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
indexNameProvider.increment();
|
||||
}
|
||||
|
||||
@Test // #3310
|
||||
@DisplayName("should evaluate static index name")
|
||||
void shouldEvaluateStaticIndexName() {
|
||||
|
||||
var persistentEntity = getRequiredPersistentEntity(EntityWithStaticName.class);
|
||||
|
||||
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo("static-name");
|
||||
}
|
||||
|
||||
@Test // #3310
|
||||
@DisplayName("should evaluate SpEL index name")
|
||||
void shouldEvaluateSpELIndexName() {
|
||||
|
||||
var persistentEntity = getRequiredPersistentEntity(EntityWithSpel.class);
|
||||
|
||||
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo(indexNameProvider.indexName());
|
||||
}
|
||||
|
||||
@Test // #3310
|
||||
@DisplayName("should evaluate property index name")
|
||||
void shouldEvaluatePropertyIndexName() {
|
||||
|
||||
var persistentEntity = getRequiredPersistentEntity(EntityWithProperty.class);
|
||||
|
||||
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo("index-property");
|
||||
}
|
||||
|
||||
private ElasticsearchPersistentEntity<?> getRequiredPersistentEntity(Class<?> clazz) {
|
||||
return operations.getElasticsearchConverter().getMappingContext().getRequiredPersistentEntity(clazz);
|
||||
}
|
||||
|
||||
@Document(indexName = "static-name")
|
||||
static class EntityWithStaticName {}
|
||||
|
||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||
static class EntityWithSpel {}
|
||||
|
||||
@Document(indexName = "${entity.indexName}")
|
||||
static class EntityWithProperty {}
|
||||
|
||||
}
|
||||
+160
@@ -36,7 +36,12 @@ import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
import org.springframework.data.elasticsearch.annotations.Setting;
|
||||
import org.springframework.data.elasticsearch.annotations.WriteTypeHint;
|
||||
import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport;
|
||||
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
|
||||
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
|
||||
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
|
||||
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
|
||||
import org.springframework.data.elasticsearch.core.index.Settings;
|
||||
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.model.FieldNamingStrategy;
|
||||
@@ -53,6 +58,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
* @author Oliver Gierke
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Steven Pearce
|
||||
*/
|
||||
public class SimpleElasticsearchPersistentEntityTests extends MappingContextBaseTests {
|
||||
|
||||
@@ -229,6 +235,34 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
|
||||
assertEquals(expected, json, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should write Default parameters to Settings object")
|
||||
void shouldWriteDefaultParametersToSettingsObject() {
|
||||
|
||||
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.get().getMappingContext()
|
||||
.getRequiredPersistentEntity(SettingDefaults.class);
|
||||
|
||||
Settings settings = entity.getDefaultSettings().flatten();
|
||||
assertThat(settings).containsEntry("index.number_of_shards", "1");
|
||||
assertThat(settings).containsEntry("index.number_of_replicas", "1");
|
||||
assertThat(settings).containsEntry("index.refresh_interval", "1s");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should write Shard and Replica parameters to Settings object")
|
||||
void shouldWriteShardReplicaToSettingsObject() {
|
||||
|
||||
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.get().getMappingContext()
|
||||
.getRequiredPersistentEntity(SettingWithShardReplicas.class);
|
||||
|
||||
Settings settings = entity.getDefaultSettings().flatten();
|
||||
assertThat(settings).containsEntry("index.number_of_shards", "4");
|
||||
assertThat(settings).containsEntry("index.number_of_replicas", "5");
|
||||
assertThat(settings).containsEntry("index.refresh_interval", "1s");
|
||||
|
||||
}
|
||||
|
||||
@Test // #3187
|
||||
@DisplayName("should evaluate SpEL expression in settingPath")
|
||||
void shouldEvaluateSpElExpressionInSettingPath() {
|
||||
@@ -248,6 +282,61 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("serverless index settings")
|
||||
@SpringJUnitConfig({ ServerlessSettingsTests.Config.class })
|
||||
class ServerlessSettingsTests {
|
||||
|
||||
@Autowired private ElasticsearchConverter elasticsearchServerlessConverter;
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
ElasticsearchConverter setupElasticsearchServerlessConverter() {
|
||||
return new MappingElasticsearchConverter(setupMappingContext());
|
||||
}
|
||||
|
||||
private SimpleElasticsearchMappingContext setupMappingContext() {
|
||||
ElasticsearchConfigurationSupport configurationSupport = new ElasticsearchConfigurationSupport();
|
||||
SimpleElasticsearchMappingContext mappingContext = configurationSupport
|
||||
.elasticsearchMappingContext(configurationSupport.elasticsearchCustomConversions());
|
||||
mappingContext.setServerType(ElasticsearchServerType.SERVERLESS);
|
||||
mappingContext.initialize();
|
||||
return mappingContext;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should write Default parameters to Settings object")
|
||||
void shouldWriteDefaultParametersToSettingsObject() {
|
||||
|
||||
ElasticsearchPersistentEntity<?> entity = elasticsearchServerlessConverter.getMappingContext()
|
||||
.getRequiredPersistentEntity(SettingDefaults.class);
|
||||
|
||||
Settings settings = entity.getDefaultSettings().flatten();
|
||||
assertThat(settings).doesNotContainKey("index.number_of_shards");
|
||||
assertThat(settings).doesNotContainKey("index.number_of_replicas");
|
||||
assertThat(settings).containsEntry("index.refresh_interval", "5s");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not write Shard and Replica parameters to Settings object")
|
||||
void shouldNotWriteShardReplicaToSettingsObject() {
|
||||
|
||||
ElasticsearchPersistentEntity<?> entity = elasticsearchServerlessConverter.getMappingContext()
|
||||
.getRequiredPersistentEntity(SettingWithShardReplicas.class);
|
||||
|
||||
Settings settings = entity.getDefaultSettings().flatten();
|
||||
|
||||
assertThat(settings).doesNotContainKey("index.number_of_shards");
|
||||
assertThat(settings).doesNotContainKey("index.number_of_replicas");
|
||||
assertThat(settings).containsEntry("index.refresh_interval", "5s");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("configuration")
|
||||
class ConfigurationTests {
|
||||
@@ -310,6 +399,57 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
|
||||
|
||||
assertThat(entity.writeTypeHints()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return Default ElasticsearchServerType from context configuration")
|
||||
void shouldReturnDefaultElasticsearchServerTypeFromContextConfiguration() {
|
||||
|
||||
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
|
||||
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
|
||||
.getRequiredPersistentEntity(FieldNameEntity.class);
|
||||
|
||||
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.DEFAULT);
|
||||
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("1s");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return ElasticsearchServerType from context configuration")
|
||||
void shouldReturnElasticsearchServerTypeFromContextConfiguration() {
|
||||
|
||||
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
|
||||
context.setServerType(ElasticsearchServerType.SERVERLESS);
|
||||
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
|
||||
.getRequiredPersistentEntity(FieldNameEntity.class);
|
||||
|
||||
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
|
||||
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("5s");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return OverriddenRefreshInterval from DEFAULT context configuration")
|
||||
void shouldReturnOverriddenRefreshIntervalFromDEFAULTContextConfiguration() {
|
||||
|
||||
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
|
||||
context.setServerType(ElasticsearchServerType.SERVERLESS);
|
||||
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
|
||||
.getRequiredPersistentEntity(SettingWithRefreshInterval.class);
|
||||
|
||||
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
|
||||
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("9s");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return OverriddenRefreshInterval from SERVERLESS context configuration")
|
||||
void shouldReturnOverriddenRefreshIntervalFromSERVERLESSContextConfiguration() {
|
||||
|
||||
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
|
||||
context.setServerType(ElasticsearchServerType.SERVERLESS);
|
||||
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
|
||||
.getRequiredPersistentEntity(SettingWithRefreshInterval.class);
|
||||
|
||||
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
|
||||
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("9s");
|
||||
}
|
||||
}
|
||||
|
||||
// region helper
|
||||
@@ -446,5 +586,25 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
|
||||
@Nullable
|
||||
@Id String id;
|
||||
}
|
||||
|
||||
@Document(indexName = "foo")
|
||||
private static class SettingDefaults {
|
||||
@Nullable
|
||||
@Id String id;
|
||||
}
|
||||
|
||||
@Document(indexName = "foo")
|
||||
@Setting(refreshInterval = "9s")
|
||||
private static class SettingWithRefreshInterval {
|
||||
@Nullable
|
||||
@Id String id;
|
||||
}
|
||||
|
||||
@Document(indexName = "foo")
|
||||
@Setting(shards = 4, replicas = 5)
|
||||
private static class SettingWithShardReplicas {
|
||||
@Nullable
|
||||
@Id String id;
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
+48
-2
@@ -89,6 +89,17 @@ class DefaultRoutingResolverUnitTest {
|
||||
assertThat(routing).isEqualTo("route 42");
|
||||
}
|
||||
|
||||
@Test // #3321
|
||||
@DisplayName("should return routing from Value expression")
|
||||
void shouldReturnRoutingFromValueExpression() {
|
||||
|
||||
ValidValueRoutingEntity entity = new ValidValueRoutingEntity("42", "route 42");
|
||||
|
||||
String routing = routingResolver.getRouting(entity);
|
||||
|
||||
assertThat(routing).isEqualTo("route 42");
|
||||
}
|
||||
|
||||
@Document(indexName = "routing-resolver-test")
|
||||
@Routing("theRouting")
|
||||
static class ValidRoutingEntity {
|
||||
@@ -151,6 +162,37 @@ class DefaultRoutingResolverUnitTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Document(indexName = "routing-resolver-test")
|
||||
@Routing(value = "#{@spelRouting.getRouting(#entity)}")
|
||||
static class ValidValueRoutingEntity {
|
||||
@Nullable
|
||||
@Id private String id;
|
||||
@Nullable private String theRouting;
|
||||
|
||||
public ValidValueRoutingEntity(@Nullable String id, @Nullable String theRouting) {
|
||||
this.id = id;
|
||||
this.theRouting = theRouting;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTheRouting() {
|
||||
return theRouting;
|
||||
}
|
||||
|
||||
public void setTheRouting(@Nullable String theRouting) {
|
||||
this.theRouting = theRouting;
|
||||
}
|
||||
}
|
||||
|
||||
@Document(indexName = "routing-resolver-test")
|
||||
@Routing("unknownProperty")
|
||||
static class InvalidRoutingEntity {
|
||||
@@ -187,8 +229,12 @@ class DefaultRoutingResolverUnitTest {
|
||||
@Nullable
|
||||
public String getRouting(Object o) {
|
||||
|
||||
if (o instanceof ValidSpelRoutingEntity) {
|
||||
return ((ValidSpelRoutingEntity) o).getTheRouting();
|
||||
if (o instanceof ValidSpelRoutingEntity e) {
|
||||
return e.getTheRouting();
|
||||
}
|
||||
|
||||
if (o instanceof ValidValueRoutingEntity e) {
|
||||
return e.getTheRouting();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#
|
||||
#
|
||||
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
|
||||
sde.testcontainers.image-version=9.4.2
|
||||
sde.testcontainers.image-version=9.5.0
|
||||
#
|
||||
#
|
||||
# needed as we do a DELETE /* at the end of the tests, will be required from 8.0 on, produces a warning since 7.13
|
||||
|
||||
Reference in New Issue
Block a user