From 01080543e9133850ecb60d1d41120356a8af18fc Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Sat, 25 Jan 2025 00:18:21 -0500 Subject: [PATCH] feat!: Spring Cloud 2024.0 and Spring Boot 3.4.2 (#3500) * chore: test Spring Cloud 2024 upgrade * fix datastore tests * fix kotlin sample test * fix spanner autoconfig * use latest spring-cloud-config in pubsub bus sample * wip pubsub stream binder test * update spring boot to 3.4.0 * adapt data-spanner to spring boot 3.4 * fix value evaluation for SqlSpannerQuery * use value expression delegate in datastore * restore unnecessary changes * fix datastore tests * format * Revert "format" This reverts commit 290c7a2dbf99a8482ee8053c9dd0030001489ef6. * fix style violations * remove unused imports * remove unused imports in autoconfigure * fix imports in pubsub emulator * fix gql datastore query test * remove spring boot 3.2 compatibility check * remove config module * remove unused module * fix secretmanager sample * fix whitespace in it * add native config for alloydb sample * update libraries bom * remove unused method in pubsub * fix 2sa usage * fix spanner units * fix units in pubsub * try new native config * use custom placeholder configurer in secretmanager * fix style in secretmanager * use static bean * add default value for secret manager sample config file * configure netty for firestore sample * fix netty usage for native image building in pubsub sample * fix native tests for data-firestore This approach avoids using @Autowired given its close-world assumption. We instead rely on @Value to resolve for the database-id property in the now fixed integration test. * fix storage sample test This uses the same approach as of data-firestore: we will not rely on autowired constructors * restore secret manager to latest of `main` We will wait for https://github.com/spring-projects/spring-framework/milestone/388 to be finished. This will translate into spring-framework:6.2.2 that will fix the regression disallowing normal use of `${sm://secret}` placeholders. * fix pubsub binder units this fix changes the way the extended properties bean is injected into the tests. With the upgrade, the bean is no w detected as two beans, producing a collision. In this new way we declare a user configuration with a custom bean. * add recommended netty config for postgress samples * fix pubsub binder tests We set a base configuration bean with mock credentials, project ID provider and extended properties * upgrade to spring-boot 3.4.1 * remove placeholder configurer from secret manager * restore datastore autoconfig this had an unintended/unnecessary change * restore graalvm config in main pom.xml This change was unnecessary * remove unused import in stream binder * cleanup of pubsub binder tests * use spring boot 3.4.1 in samples * remove native config for alloydb sample * remove unused whitespace in poms * Revert "fix storage sample test" This reverts commit dd81242627975cab16083263fa689cd2361d73ac. * try using unchanged firestore IT config * Revert "fix pubsub binder tests" This reverts commit 7b8240041252076173ad52f7d5e96a5ae0d04a69. * Reapply "fix pubsub binder tests" This reverts commit be28c7278c81188c0ed44b05ad5a4a97e3a93a65. * test reverting changes in pubsub binder * Revert "test reverting changes in pubsub binder" This reverts commit 4eae5e74a79b44cd5412afb054e5d28991657009. * add support for value expressions and query method evaluators in datastore * test both value expressions and query method expressions in datastore * fix style * fix style ii * introduce support for value expression delegate and query method evaluation context in spanner * adapt spanner tests * standardize to parameterized booleans * add support info on root readme * add spring cloud 2024 support info in gettin-started * introduce 5.x to 6.x migration guide * update project.version to 6.0.0-SNAPSHOT * Revert "remove config module" This reverts commit 66b4439b04f88c61507f1969c89b1d8198c4e517. * update project.version in config integrations * update versions.txt * update project.version in spring-cloud-generator * retrigger tests * fix checkstyle violations in spanner * fix checkstyle violations in datastore * update valkey to 6.x * fix checkstyle in data-spanner * replace golden copyright year * Revert "update valkey to 6.x" This reverts commit eaf04dcd0fe2f132219e6217ec8e62c08f01c823. * Revert "update project.version in spring-cloud-generator" This reverts commit 0fabdbfcbe4cc944010d73481eb6c5a8e923ebf1. * Revert "update versions.txt" This reverts commit 3eeef1eb8cb9e38375ec94c8a813a8b7cadd44e6. * Revert "update project.version in config integrations" This reverts commit ce401259b2e80c2c44e99ac3a89d7b9eabe3deed. * Revert "update project.version to 6.0.0-SNAPSHOT" This reverts commit 4015f3f2316fc845fd0527c8b180b6315820c2c7. * update secretmanager sample * update secret manager to support sm@ and sm:// syntax * add parameterized tests for sm:// and sm@ in secretmanager * adapt secret manager autoconfig tests to new prefix * abort full string attempt in secret manager this prevents attempts of the form sm@my_secret:default as a whole string * consider sm:// when exiting on full string match attempts * finish sample code * add functional interface annotation to PrefixMatcher * make secret manager dependency non-optional in autoconfigure module * cleanup * fix refresh scope for secretmanager sample This moves the @RefreshScope annotation to method-level in order to avoid build failures in the native image tests. A good candidate to prove it as good practice is https://github.com/spring-cloud/spring-cloud-commons/blob/c0f3a6190c5e636ac5391371b503f7f1e6e24a8e/docs/modules/ROOT/pages/spring-cloud-commons/application-context-services.adoc?plain=1#L211-L215 The native tests script will also override the refresh scope as it's explained in the docs (see implementation for link). * update spring-boot to 3.4.2 --- .github/workflows/integrationTests.yaml | 108 ------ .../workflows/scripts/native-image-tests.sh | 6 +- README.adoc | 6 + docs/src/main/asciidoc/getting-started.adoc | 2 + .../main/asciidoc/migration-guide-5.x.adoc | 65 ++++ pom.xml | 4 +- spring-cloud-gcp-autoconfigure/pom.xml | 5 +- ...cretManagerConfigDataLocationResolver.java | 16 +- .../SecretManagerCompatibilityTests.java | 33 +- ...erConfigDataLocationResolverUnitTests.java | 27 +- .../query/DatastoreQueryLookupStrategy.java | 37 +- .../repository/query/GqlDatastoreQuery.java | 110 ++++-- .../support/DatastoreRepositoryFactory.java | 25 ++ .../DatastoreQueryLookupStrategyTests.java | 55 ++- .../query/GqlDatastoreQueryTests.java | 140 +++++--- .../src/test/resources/index.yaml | 1 + .../query/SpannerQueryLookupStrategy.java | 39 +- .../repository/query/SqlSpannerQuery.java | 42 ++- .../support/SpannerRepositoryFactory.java | 23 ++ .../SpannerPersistentPropertyImplTests.java | 1 - .../SpannerQueryLookupStrategyTests.java | 65 ++-- .../query/SqlSpannerQueryTests.java | 161 +++++---- .../SpannerRepositoryFactoryTests.java | 5 + .../test/AbstractSpannerIntegrationTest.java | 1 - .../stream/binder/pubsub/PubSubEmulator.java | 22 +- .../PubSubMessageChannelBinderTests.java | 36 +- spring-cloud-gcp-samples/pom.xml | 3 +- .../main/kotlin/com/example/data/Person.kt | 2 +- .../java/com/example/SecretConfiguration.java | 3 +- .../example/SecretManagerWebController.java | 25 +- .../src/main/resources/application.properties | 11 +- .../SecretManagerPropertyUtils.java | 35 +- .../SecretManagerSyntaxUtils.java | 51 +++ .../SecretManagerPropertyUtilsTests.java | 55 ++- .../SecretManagerSyntaxUtilsTest.java | 333 ++++++++++++++++++ 35 files changed, 1180 insertions(+), 373 deletions(-) create mode 100644 docs/src/main/asciidoc/migration-guide-5.x.adoc create mode 100644 spring-cloud-gcp-secretmanager/src/main/java/com/google/cloud/spring/secretmanager/SecretManagerSyntaxUtils.java create mode 100644 spring-cloud-gcp-secretmanager/src/test/java/com/google/cloud/spring/secretmanager/SecretManagerSyntaxUtilsTest.java diff --git a/.github/workflows/integrationTests.yaml b/.github/workflows/integrationTests.yaml index a37c948d12..e4e26bcf52 100644 --- a/.github/workflows/integrationTests.yaml +++ b/.github/workflows/integrationTests.yaml @@ -110,114 +110,6 @@ jobs: --batch-mode \ --define aggregate=true \ surefire-report:failsafe-report-only - - name: Archive logs - if: always() - continue-on-error: true - uses: actions/upload-artifact@v4 - with: - name: Integration Test Logs - ${{ matrix.it}} - path: | - **/target/failsafe-reports/* - **/target/site - - spring-boot-3-2-compatibility: - if: | - github.actor != 'dependabot[bot]' && (( - github.event_name == 'pull_request' && github.repository == github.event.pull_request.head.repo.full_name - ) || (github.event_name != 'pull_request')) - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - it: - - alloydb - - bigquery - - cloudsql - - config - - datastore - - firestore - - kms - - kotlin - - logging - - metrics - - multisample - - pubsub - - pubsub-bus - - pubsub-docs - - pubsub-emulator - - pubsub-integration - - secretmanager - - spanner - - storage - # trace excluded - - vision - steps: - - name: Get current date - id: date - run: echo "date=$(date +'%Y-%m-%d' --utc)" >> $GITHUB_OUTPUT - - uses: actions/checkout@v2 - - name: Setup Java 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 17 - - name: Set Up Authentication - uses: google-github-actions/auth@v1 - with: - credentials_json: ${{ secrets.SPRING_CLOUD_GCP_CI_SA_KEY }} - - name: Setup gcloud - uses: google-github-actions/setup-gcloud@v1 - with: - version: latest - project_id: spring-cloud-gcp-ci - export_default_credentials: true - - name: Install pubsub-emulator - if: ${{ matrix.it == 'pubsub-emulator' }} - run: | - gcloud components install pubsub-emulator beta && \ - gcloud components update - - name: Mvn install # Need this when the directory/pom structure changes - id: install - run: | - ./mvnw \ - --batch-mode \ - --no-transfer-progress \ - --threads 1.5C \ - --define maven.test.skip=true \ - --define maven.javadoc.skip=true \ - --define org.slf4j.simpleLogger.showDateTime=true \ - --define org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss:SSS \ - install - - name: Update samples to use Spring Boot Starter 3.2.5 - working-directory: ./spring-cloud-gcp-samples - run: | - sudo apt-get update && sudo apt-get install -y xmlstarlet - xmlstarlet ed -L -N x=http://maven.apache.org/POM/4.0.0 \ - -u '/x:project/x:parent/x:version' -v '3.2.5' pom.xml - - name: Integration Tests - id: intTest - env: - DB_PASSWORD: ${{ secrets.SPRING_CLOUD_GCP_CI_DB_ROOT_PASSWORD }} - run: | - ./mvnw \ - --batch-mode \ - --no-transfer-progress \ - --activate-profiles spring-cloud-gcp-ci-it \ - --define maven.javadoc.skip=true \ - --define skip.surefire.tests=true \ - --define org.slf4j.simpleLogger.showDateTime=true \ - --define org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss:SSS \ - --define "spring.datasource.password=${DB_PASSWORD}" \ - --define "spring.r2dbc.password=${DB_PASSWORD}" \ - --define "spring.cloud.gcp.sql.database-name=code_samples_test_db2" \ - --define it.${{ matrix.it }}=true \ - verify - - name: Aggregate Report - run: | - ./mvnw \ - --batch-mode \ - --define aggregate=true \ - surefire-report:failsafe-report-only - name: Archive logs if: always() continue-on-error: true diff --git a/.github/workflows/scripts/native-image-tests.sh b/.github/workflows/scripts/native-image-tests.sh index 9de8860052..1439c54b2e 100755 --- a/.github/workflows/scripts/native-image-tests.sh +++ b/.github/workflows/scripts/native-image-tests.sh @@ -75,13 +75,17 @@ run_sample_tests () { else project_names="$(echo "${module_samples[@]}" | sed 's/ /,/g')" + # Note that spring.cloud.refresh is disabled in native image mode. This affects samples + # that use @RefreshScope. + # See https://docs.spring.io/spring-cloud-config/reference/client.html#aot-and-native-image-support mvn clean test \ --activate-profiles native-sample-config,nativeTest \ --define notAllModules=true \ --define maven.javadoc.skip=true \ -pl="${project_names}" \ --define org.slf4j.simpleLogger.showDateTime=true \ - --define org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss:SSS + --define org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss:SSS \ + --define spring-boot.run.arguments="--spring.cloud.refresh.enabled=false" fi popd } diff --git a/README.adoc b/README.adoc index 74a5967452..0ff189b6fe 100644 --- a/README.adoc +++ b/README.adoc @@ -54,6 +54,12 @@ This project has dependency and transitive dependencies on Spring Projects. The |=== | Spring Framework on Google Cloud | Spring Cloud | Spring Boot | Spring Framework | Supported +|6.x +|https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2024.0-Release-Notes[2024.0.x] (Moorgate) +|https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.4-Release-Notes[3.4.x] +|https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-6.2-Release-Notes[6.2.x] +|Yes + |5.x |https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2023.0-Release-Notes[2023.0.x] (Leyton) |https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes[3.2.x]*, https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.3-Release-Notes[3.3.x] diff --git a/docs/src/main/asciidoc/getting-started.adoc b/docs/src/main/asciidoc/getting-started.adoc index c31fac4d94..290927fc36 100644 --- a/docs/src/main/asciidoc/getting-started.adoc +++ b/docs/src/main/asciidoc/getting-started.adoc @@ -8,6 +8,8 @@ Spring Framework on Google Cloud has dependency and transitive dependencies on S |=== | Spring Framework on Google Cloud | Spring Cloud | Spring Boot | Spring Framework | Supported + +|6.x |https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2024.0-Release-Notes[2024.0.x] (Moorgate)|https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.4-Release-Notes[3.4.x]|https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-6.2-Release-Notes[6.2.x]|Yes |5.x | https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2023.0-Release-Notes[2023.0.x] (Leyton) |https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes[3.2.x]*, https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.3-Release-Notes[3.3.x] | https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-Spring-Framework-6.x#whats-new-in-version-61[6.1.x]| Yes |4.x | https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2022.0-Release-Notes[2022.0.x] (Kilburn) |https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Release-Notes[3.0.x], https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.1-Release-Notes[3.1.x]| https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-Spring-Framework-6.x#whats-new-in-version-60[6.x]| Yes diff --git a/docs/src/main/asciidoc/migration-guide-5.x.adoc b/docs/src/main/asciidoc/migration-guide-5.x.adoc new file mode 100644 index 0000000000..ed6905e249 --- /dev/null +++ b/docs/src/main/asciidoc/migration-guide-5.x.adoc @@ -0,0 +1,65 @@ +== Migration Guide from Spring Cloud GCP 5.x to 6.x +=== Before you start +==== Upgrade to the 5.x version +This doc assumes you are running with Spring Cloud GCP 5.x. + +If you are currently running with an earlier major version of Spring Cloud GCP, i.e., 1.x or 2.x, we recommend that you upgrade to [Spring Cloud GCP 5.x] before migrating to Spring Cloud GCP 6.x. + +* link:migration-guide-1.x.adoc[Migration guide from Spring Cloud GCP 1.x to 2.x] +* link:migration-guide-3.x.adoc[Migration guide from Spring Cloud GCP 3.x to 4.x] + +Note that since Spring Cloud GCP 5.0 has few breaking changes, we have only provided release notes +as reference. + +==== Review System requirements +Spring Cloud GCP 6.0 is built on Spring Boot 3.4.x and Spring Framework 6.2.x, which requires Java 17 at minimum. +If you are currently on Java 8 or Java 11, you need to upgrade your JDK before you can develop an application based on Spring Cloud GCP 6.0. + +=== Upgrade to Spring Cloud GCP 6.0 +==== Update Bill of Materials (BOM) +If you’re a Maven user, add our BOM to your pom.xml `` section. +This will allow you to not specify versions for any of the Maven dependencies and instead delegate versioning to the BOM. + +[source, xml] +---- + + + + com.google.cloud + spring-cloud-gcp-dependencies + 6.0.0 + pom + import + + + +---- + +===== Review Dependencies +Run `mvn dependency:tree` (`gradlew dependencies` for Gradle projects) to see the dependency tree of your project. +Ensure Spring-related dependencies have matching versions: + +* Spring Boot 3.4.x +* Spring Cloud 2024.x +** For detailed dependency versions, see "2024.0 (Moorgate)" column in link:https://github.com/spring-cloud/spring-cloud-release/wiki/Supported-Versions#supported-releases[Spring Cloud: Supported Versions] table. + +==== Secret Manager +Introduced support for the `${sm@secret_id}` syntax. This new syntax is encouraged over the (still +supported) `${sm://secret_id}` syntax. Users still relying on the `sm://` syntax will be issued a +warning. + +For example: +[source, java] +@Value("${sm@my_secret}") +String mySecret; +--- + +==== Datastore +Introduced support for `ValueExpressionDelegate`. This class is suggested over +`QueryMethodEvaluationContextProvider`. + +==== Spanner +Introduced support for `ValueExpressionDelegate`. This class is suggested over +`QueryMethodEvaluationContextProvider`. + + diff --git a/pom.xml b/pom.xml index 642d80e4ec..aa2da1ce83 100644 --- a/pom.xml +++ b/pom.xml @@ -36,8 +36,8 @@ ${project.version} - 2023.0.5 - 3.3.4 + 2024.0.0 + 3.4.2 ${project.parent.version} 2.2.6 2.5.0 diff --git a/spring-cloud-gcp-autoconfigure/pom.xml b/spring-cloud-gcp-autoconfigure/pom.xml index 64eeeb48c1..cb57aab0eb 100644 --- a/spring-cloud-gcp-autoconfigure/pom.xml +++ b/spring-cloud-gcp-autoconfigure/pom.xml @@ -275,7 +275,10 @@ com.google.cloud spring-cloud-gcp-secretmanager - true + + + + false diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java index f75c9a1d1f..3b58a9d55d 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolver.java @@ -16,6 +16,9 @@ package com.google.cloud.spring.autoconfigure.secretmanager; +import static com.google.cloud.spring.secretmanager.SecretManagerSyntaxUtils.getMatchedPrefixes; +import static com.google.cloud.spring.secretmanager.SecretManagerSyntaxUtils.warnIfUsingDeprecatedSyntax; + import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; import com.google.cloud.spring.core.DefaultCredentialsProvider; @@ -26,7 +29,10 @@ import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.apache.arrow.util.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.BootstrapRegistry; import org.springframework.boot.context.config.ConfigDataLocation; @@ -38,10 +44,8 @@ public class SecretManagerConfigDataLocationResolver implements ConfigDataLocationResolver { - /** - * ConfigData Prefix for Google Cloud Secret Manager. - */ - public static final String PREFIX = "sm://"; + private static final Logger logger = LoggerFactory.getLogger(SecretManagerConfigDataLocationResolver.class); + /** * A static client to avoid creating another client after refreshing. */ @@ -50,7 +54,9 @@ public class SecretManagerConfigDataLocationResolver implements @Override public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { - return location.hasPrefix(PREFIX); + Optional matchedPrefix = getMatchedPrefixes(location::hasPrefix); + warnIfUsingDeprecatedSyntax(logger, matchedPrefix.orElse("")); + return matchedPrefix.isPresent(); } @Override diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerCompatibilityTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerCompatibilityTests.java index bd807f5600..a2fbcf898f 100644 --- a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerCompatibilityTests.java +++ b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerCompatibilityTests.java @@ -11,8 +11,12 @@ import com.google.cloud.secretmanager.v1.SecretPayload; import com.google.cloud.secretmanager.v1.SecretVersionName; import com.google.protobuf.ByteString; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.BootstrapRegistry.InstanceSupplier; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; @@ -28,6 +32,13 @@ class SecretManagerCompatibilityTests { private SpringApplicationBuilder application; private SecretManagerServiceClient client; + static Stream prefixes() { + return Stream.of( + Arguments.of("sm://"), + Arguments.of("sm@") + ); + } + @BeforeEach void init() { application = new SpringApplicationBuilder(SecretManagerCompatibilityTests.class) @@ -64,10 +75,11 @@ void init() { * com.google.cloud.spring.secretmanager.SecretManagerTemplate} autoconfiguration and properties * resolved. */ - @Test - void testConfigurationWhenDefaultSecretIsNotAllowed() { + @ParameterizedTest + @MethodSource("prefixes") + void testConfigurationWhenDefaultSecretIsNotAllowed(String prefix) { application.properties( - "spring.config.import=sm://") + "spring.config.import=" + prefix) .addBootstrapRegistryInitializer( (registry) -> registry.registerIfAbsent( SecretManagerServiceClient.class, @@ -76,17 +88,18 @@ void testConfigurationWhenDefaultSecretIsNotAllowed() { ); try (ConfigurableApplicationContext applicationContext = application.run()) { ConfigurableEnvironment environment = applicationContext.getEnvironment(); - assertThat(environment.getProperty("sm://my-secret")).isEqualTo("newSecret"); - assertThatThrownBy(() -> environment.getProperty("sm://fake-secret")) + assertThat(environment.getProperty(prefix + "my-secret")).isEqualTo("newSecret"); + assertThatThrownBy(() -> environment.getProperty(prefix + "fake-secret")) .isExactlyInstanceOf(NotFoundException.class); } } - @Test - void testConfigurationWhenDefaultSecretIsAllowed() { + @ParameterizedTest + @MethodSource("prefixes") + void testConfigurationWhenDefaultSecretIsAllowed(String prefix) { application.properties( "spring.cloud.gcp.secretmanager.allow-default-secret=true", - "spring.config.import=sm://") + "spring.config.import=" + prefix) .addBootstrapRegistryInitializer( (registry) -> registry.registerIfAbsent( SecretManagerServiceClient.class, @@ -95,8 +108,8 @@ void testConfigurationWhenDefaultSecretIsAllowed() { ); try (ConfigurableApplicationContext applicationContext = application.run()) { ConfigurableEnvironment environment = applicationContext.getEnvironment(); - assertThat(environment.getProperty("sm://my-secret")).isEqualTo("newSecret"); - assertThat(environment.getProperty("sm://fake-secret")).isNull(); + assertThat(environment.getProperty(prefix + "my-secret")).isEqualTo("newSecret"); + assertThat(environment.getProperty(prefix + "fake-secret")).isNull(); } } } diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolverUnitTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolverUnitTests.java index 7bb032548f..d66da3cdab 100644 --- a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolverUnitTests.java +++ b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/secretmanager/SecretManagerConfigDataLocationResolverUnitTests.java @@ -8,8 +8,12 @@ import com.google.api.gax.core.CredentialsProvider; import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.BootstrapRegistry; import org.springframework.boot.DefaultBootstrapContext; @@ -28,15 +32,23 @@ class SecretManagerConfigDataLocationResolverUnitTests { ConfigDataLocationResolverContext.class); private final DefaultBootstrapContext defaultBootstrapContext = new DefaultBootstrapContext(); + static Stream prefixes() { + return Stream.of( + Arguments.of("sm://"), + Arguments.of("sm@") + ); + } + @Test void isResolvableReturnsFalseWithIncorrectPrefix() { assertThat(resolver.isResolvable(context, ConfigDataLocation.of("test://"))).isFalse(); assertThat(resolver.isResolvable(context, ConfigDataLocation.of("sm:"))).isFalse(); } - @Test - void isResolvableReturnsFalseWithCorrectPrefix() { - assertThat(resolver.isResolvable(context, ConfigDataLocation.of("sm://"))).isTrue(); + @ParameterizedTest + @MethodSource("prefixes") + void isResolvableReturnsFalseWithCorrectPrefix(String prefix) { + assertThat(resolver.isResolvable(context, ConfigDataLocation.of(prefix))).isTrue(); } @Test @@ -48,13 +60,14 @@ void createSecretManagerClientWithPresetClientTest() { .isEqualTo(client); } - @Test - void resolveReturnsConfigDataLocation() { + @ParameterizedTest + @MethodSource("prefixes") + void resolveReturnsConfigDataLocation(String prefix) { List locations = resolver.resolve(context, - ConfigDataLocation.of("sm://my-secret")); + ConfigDataLocation.of(prefix + "my-secret")); assertThat(locations).hasSize(1); assertThat(locations).first().extracting("location") - .isEqualTo(ConfigDataLocation.of("sm://my-secret")); + .isEqualTo(ConfigDataLocation.of(prefix + "my-secret")); ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class); when(applicationContext.getBeanFactory()).thenReturn(new DefaultListableBeanFactory()); assertThatCode( diff --git a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategy.java b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategy.java index e8d2de103e..80becb0aac 100644 --- a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategy.java +++ b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategy.java @@ -26,6 +26,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.Assert; /** @@ -39,17 +40,34 @@ public class DatastoreQueryLookupStrategy implements QueryLookupStrategy { private final DatastoreMappingContext datastoreMappingContext; - private QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; + + @SuppressWarnings("deprecation") + private final QueryMethodEvaluationContextProvider queryEvaluationContextProvider; public DatastoreQueryLookupStrategy( DatastoreMappingContext datastoreMappingContext, DatastoreOperations datastoreOperations, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + ValueExpressionDelegate valueExpressionDelegate) { Assert.notNull(datastoreMappingContext, "A non-null DatastoreMappingContext is required."); Assert.notNull(datastoreOperations, "A non-null DatastoreOperations is required."); - Assert.notNull(evaluationContextProvider, "A non-null EvaluationContextProvider is required."); + Assert.notNull(valueExpressionDelegate, "A non-null ValueExpressionDelegate is required."); this.datastoreMappingContext = datastoreMappingContext; - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = valueExpressionDelegate; + this.queryEvaluationContextProvider = null; + this.datastoreOperations = datastoreOperations; + } + + public DatastoreQueryLookupStrategy( + DatastoreMappingContext datastoreMappingContext, + DatastoreOperations datastoreOperations, + @SuppressWarnings("deprecation") QueryMethodEvaluationContextProvider queryEvaluationContextProvider) { + Assert.notNull(datastoreMappingContext, "A non-null DatastoreMappingContext is required."); + Assert.notNull(datastoreOperations, "A non-null DatastoreOperations is required."); + Assert.notNull(queryEvaluationContextProvider, "A non-null EvaluationContextProvider is required."); + this.datastoreMappingContext = datastoreMappingContext; + this.valueExpressionDelegate = null; + this.queryEvaluationContextProvider = queryEvaluationContextProvider; this.datastoreOperations = datastoreOperations; } @@ -80,12 +98,21 @@ public RepositoryQuery resolveQuery( GqlDatastoreQuery createGqlDatastoreQuery( Class entityType, DatastoreQueryMethod queryMethod, String gql) { + if (valueExpressionDelegate != null) { + return new GqlDatastoreQuery<>( + entityType, + queryMethod, + this.datastoreOperations, + gql, + this.valueExpressionDelegate, + this.datastoreMappingContext); + } return new GqlDatastoreQuery<>( entityType, queryMethod, this.datastoreOperations, gql, - this.evaluationContextProvider, + this.queryEvaluationContextProvider, this.datastoreMappingContext); } diff --git a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQuery.java b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQuery.java index 0799095afc..28dab76d56 100644 --- a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQuery.java +++ b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQuery.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -59,6 +60,8 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.SpelEvaluator; import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.util.StringUtils; /** @@ -82,10 +85,40 @@ public class GqlDatastoreQuery extends AbstractDatastoreQuery { private List originalParamTags; - private QueryMethodEvaluationContextProvider evaluationContextProvider; + private final ValueExpressionDelegate valueExpressionDelegate; + + private final QueryMethodEvaluationContextProvider queryEvaluationContextProvider; + + private ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter valueExpressionQueryRewriter; private SpelQueryContext.EvaluatingSpelQueryContext evaluatingSpelQueryContext; + /** + * Constructor. + * + * @param type the underlying entity type + * @param queryMethod the underlying query method to support. + * @param datastoreTemplate used for executing queries. + * @param gql the query text. + * @param valueExpressionDelegate the provider used to evaluate SpEL expressions in queries. + * @param datastoreMappingContext used for getting metadata about entities. + */ + public GqlDatastoreQuery( + Class type, + DatastoreQueryMethod queryMethod, + DatastoreOperations datastoreTemplate, + String gql, + ValueExpressionDelegate valueExpressionDelegate, + DatastoreMappingContext datastoreMappingContext) { + super(queryMethod, datastoreTemplate, datastoreMappingContext, type); + this.valueExpressionDelegate = valueExpressionDelegate; + this.queryEvaluationContextProvider = null; + this.originalGql = StringUtils.trimTrailingCharacter(gql.trim(), ';'); + setOriginalParamTags(); + setEvaluatingSpelQueryContext(); + setGqlResolvedEntityClassName(); + } + /** * Constructor. * @@ -104,7 +137,8 @@ public GqlDatastoreQuery( QueryMethodEvaluationContextProvider evaluationContextProvider, DatastoreMappingContext datastoreMappingContext) { super(queryMethod, datastoreTemplate, datastoreMappingContext, type); - this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = null; + this.queryEvaluationContextProvider = evaluationContextProvider; this.originalGql = StringUtils.trimTrailingCharacter(gql.trim(), ';'); setOriginalParamTags(); setEvaluatingSpelQueryContext(); @@ -312,22 +346,30 @@ private void setGqlResolvedEntityClassName() { this.gqlResolvedEntityClassName = result; } + @SuppressWarnings("deprecation") private void setEvaluatingSpelQueryContext() { Set originalTags = new HashSet<>(GqlDatastoreQuery.this.originalParamTags); - - GqlDatastoreQuery.this.evaluatingSpelQueryContext = - SpelQueryContext.of( - (counter, spelExpression) -> { - String newTag; - do { - counter++; - newTag = "@SpELtag" + counter; - } while (originalTags.contains(newTag)); - originalTags.add(newTag); - return newTag; - }, - (prefix, newTag) -> newTag) - .withEvaluationContextProvider(GqlDatastoreQuery.this.evaluationContextProvider); + BiFunction parameterNameSource = (Integer counter, String spelExpression) -> { + String newTag; + do { + counter++; + newTag = "@SpELtag" + counter; + } while (originalTags.contains(newTag)); + originalTags.add(newTag); + return newTag; + }; + // We favor ValueExpressionDelegate since it's not deprecated + if (valueExpressionDelegate != null) { + GqlDatastoreQuery.this.valueExpressionQueryRewriter = ValueExpressionQueryRewriter.of(valueExpressionDelegate, + parameterNameSource, (left, right) -> right) + .withEvaluationContextAccessor(valueExpressionDelegate.getEvaluationContextAccessor()); + } else { + GqlDatastoreQuery.this.evaluatingSpelQueryContext = + SpelQueryContext.of( + parameterNameSource, + (prefix, newTag) -> newTag) + .withEvaluationContextProvider(GqlDatastoreQuery.this.queryEvaluationContextProvider); + } } // Convenience class to hold a grouping of GQL, tags, and parameter values. @@ -352,6 +394,32 @@ private class ParsedQueryWithTagsAndValues { int limitPosition; + Map evaluationResults; + + /** + * This method prepares the Gql query and its evaluation results. It will favor + * {@link ValueExpressionDelegate} over the deprecated + * {@link QueryMethodEvaluationContextProvider}. + */ + @SuppressWarnings("deprecation") + private void evaluateGql() { + if (GqlDatastoreQuery.this.valueExpressionDelegate != null) { + ValueExpressionQueryRewriter.QueryExpressionEvaluator spelEvaluator = + GqlDatastoreQuery.this.valueExpressionQueryRewriter.parse( + GqlDatastoreQuery.this.gqlResolvedEntityClassName, + GqlDatastoreQuery.this.queryMethod.getParameters()); + this.evaluationResults = spelEvaluator.evaluate(this.rawParams); + this.finalGql = spelEvaluator.getQueryString(); + } else { + SpelEvaluator spelEvaluator = + GqlDatastoreQuery.this.evaluatingSpelQueryContext.parse( + GqlDatastoreQuery.this.gqlResolvedEntityClassName, + GqlDatastoreQuery.this.queryMethod.getParameters()); + this.evaluationResults = spelEvaluator.evaluate(this.rawParams); + this.finalGql = spelEvaluator.getQueryString(); + } + } + ParsedQueryWithTagsAndValues(List initialTags, Object[] rawParams) { this.params = Arrays.stream(rawParams) @@ -359,15 +427,9 @@ private class ParsedQueryWithTagsAndValues { .collect(Collectors.toList()); this.rawParams = rawParams; this.tagsOrdered = new ArrayList<>(initialTags); + evaluateGql(); - SpelEvaluator spelEvaluator = - GqlDatastoreQuery.this.evaluatingSpelQueryContext.parse( - GqlDatastoreQuery.this.gqlResolvedEntityClassName, - GqlDatastoreQuery.this.queryMethod.getParameters()); - Map results = spelEvaluator.evaluate(this.rawParams); - this.finalGql = spelEvaluator.getQueryString(); - - for (Map.Entry entry : results.entrySet()) { + for (Map.Entry entry : this.evaluationResults.entrySet()) { this.params.add(entry.getValue()); // Cloud Datastore requires the tag name without the @ this.tagsOrdered.add(entry.getKey().substring(1)); diff --git a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/support/DatastoreRepositoryFactory.java b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/support/DatastoreRepositoryFactory.java index e0a1f2e0d9..0925031fe4 100644 --- a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/support/DatastoreRepositoryFactory.java +++ b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/support/DatastoreRepositoryFactory.java @@ -36,6 +36,8 @@ import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -97,6 +99,22 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { } @Override + protected Optional getQueryLookupStrategy( + @Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { + + return Optional.of( + new DatastoreQueryLookupStrategy( + this.datastoreMappingContext, + this.datastoreOperations, + valueExpressionDelegate)); + } + + /** + * @deprecated in favor of {@link #getQueryLookupStrategy(Key, ValueExpressionDelegate)} + */ + @Override + @SuppressWarnings("deprecation") + @Deprecated(since = "6.0") protected Optional getQueryLookupStrategy( @Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { @@ -105,6 +123,7 @@ protected Optional getQueryLookupStrategy( this.datastoreMappingContext, this.datastoreOperations, delegateContextProvider(evaluationContextProvider))); + } @Override @@ -112,6 +131,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws this.applicationContext = applicationContext; } + @SuppressWarnings("deprecation") private QueryMethodEvaluationContextProvider delegateContextProvider( QueryMethodEvaluationContextProvider evaluationContextProvider) { @@ -143,6 +163,11 @@ private QueryMethodEvaluationContextProvider delegateContextProvider( new BeanFactoryResolver(DatastoreRepositoryFactory.this.applicationContext)); return evaluationContext; } + + @Override + public EvaluationContextProvider getEvaluationContextProvider() { + return (EvaluationContextProvider) evaluationContextProvider; + } }; } } diff --git a/spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategyTests.java b/spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategyTests.java index 88ab23b72a..314ebed614 100644 --- a/spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategyTests.java +++ b/spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategyTests.java @@ -31,12 +31,15 @@ import com.google.cloud.spring.data.datastore.core.mapping.DatastoreMappingContext; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** Tests for the Query Method lookup class. */ class DatastoreQueryLookupStrategyTests { @@ -47,7 +50,7 @@ class DatastoreQueryLookupStrategyTests { private DatastoreQueryMethod queryMethod; - private DatastoreQueryLookupStrategy datastoreQueryLookupStrategy; + private ValueExpressionDelegate valueExpressionDelegate; private QueryMethodEvaluationContextProvider evaluationContextProvider; @@ -56,12 +59,17 @@ void initMocks() { this.datastoreTemplate = mock(DatastoreTemplate.class); this.datastoreMappingContext = new DatastoreMappingContext(); this.queryMethod = mock(DatastoreQueryMethod.class); + this.valueExpressionDelegate = mock(ValueExpressionDelegate.class); this.evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); - this.datastoreQueryLookupStrategy = getDatastoreQueryLookupStrategy(); } - @Test - void resolveSqlQueryTest() { + /** + * int parameters are used as indexes of the two lookup strategies we use + */ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void resolveSqlQueryTest(boolean useValueExpressionDelegate) { + DatastoreQueryLookupStrategy lookupStrategy = getDatastoreQueryLookupStrategy(useValueExpressionDelegate); String queryName = "fakeNamedQueryName"; String query = "fake query"; when(this.queryMethod.getNamedQueryName()).thenReturn(queryName); @@ -85,24 +93,45 @@ void resolveSqlQueryTest() { when(namedQueries.hasQuery(queryName)).thenReturn(true); when(namedQueries.getQuery(queryName)).thenReturn(query); + when(valueExpressionDelegate.getEvaluationContextAccessor()).thenReturn(mock(QueryMethodValueEvaluationContextAccessor.class)); - this.datastoreQueryLookupStrategy.resolveQuery(null, null, null, namedQueries); + lookupStrategy.resolveQuery(null, null, null, namedQueries); - verify(this.datastoreQueryLookupStrategy, times(1)) + verify(lookupStrategy, times(1)) .createGqlDatastoreQuery(eq(Object.class), same(this.queryMethod), eq(query)); } - private DatastoreQueryLookupStrategy getDatastoreQueryLookupStrategy() { - DatastoreQueryLookupStrategy spannerQueryLookupStrategy = + private DatastoreQueryLookupStrategy getDatastoreQueryLookupStrategy(boolean useValueExpressionDelegate) { + return useValueExpressionDelegate + ? getDatastoreQueryLookupStrategy(this.valueExpressionDelegate) + : getDatastoreQueryLookupStrategy(this.evaluationContextProvider); + } + + private DatastoreQueryLookupStrategy getDatastoreQueryLookupStrategy(ValueExpressionDelegate valueExpressionDelegate) { + DatastoreQueryLookupStrategy lookupStrategy = + spy( + new DatastoreQueryLookupStrategy( + this.datastoreMappingContext, + this.datastoreTemplate, + valueExpressionDelegate)); + return prepareDatastoreQueryLookupStrategy(lookupStrategy); + } + + private DatastoreQueryLookupStrategy getDatastoreQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluationContextProvider) { + DatastoreQueryLookupStrategy lookupStrategy = spy( new DatastoreQueryLookupStrategy( this.datastoreMappingContext, this.datastoreTemplate, - this.evaluationContextProvider)); - doReturn(Object.class).when(spannerQueryLookupStrategy).getEntityType(any()); + evaluationContextProvider)); + return prepareDatastoreQueryLookupStrategy(lookupStrategy); + } + + private DatastoreQueryLookupStrategy prepareDatastoreQueryLookupStrategy(DatastoreQueryLookupStrategy base) { + doReturn(Object.class).when(base).getEntityType(any()); doReturn(this.queryMethod) - .when(spannerQueryLookupStrategy) + .when(base) .createQueryMethod(any(), any(), any()); - return spannerQueryLookupStrategy; + return base; } } diff --git a/spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQueryTests.java b/spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQueryTests.java index c0e5e46645..17ff0602f1 100644 --- a/spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQueryTests.java +++ b/spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQueryTests.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; @@ -48,14 +47,20 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Stream; import org.assertj.core.data.Offset; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -64,6 +69,8 @@ import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -83,6 +90,8 @@ class GqlDatastoreQueryTests { private DatastoreQueryMethod queryMethod; + private ValueExpressionDelegate valueExpressionDelegate; + private QueryMethodEvaluationContextProvider evaluationContextProvider; @BeforeEach @@ -96,27 +105,47 @@ void initMocks() { when(this.datastoreTemplate.getDatastoreEntityConverter()) .thenReturn(this.datastoreEntityConverter); when(this.datastoreEntityConverter.getConversions()).thenReturn(this.readWriteConversions); + this.valueExpressionDelegate = mock(ValueExpressionDelegate.class); + when(valueExpressionDelegate.getEvaluationContextAccessor()).thenReturn(mock(QueryMethodValueEvaluationContextAccessor.class)); this.evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); } + private GqlDatastoreQuery createQuerySpy( + String gql, boolean isPageQuery, boolean isSliceQuery, boolean useValueExpressionDelegate) { + GqlDatastoreQuery spy; + if (useValueExpressionDelegate) { + spy = + spy( + new GqlDatastoreQuery<>( + Trade.class, + this.queryMethod, + this.datastoreTemplate, + gql, + this.valueExpressionDelegate, + this.datastoreMappingContext)); + } else { + spy = spy(new GqlDatastoreQuery<>( + Trade.class, + this.queryMethod, + this.datastoreTemplate, + gql, + this.evaluationContextProvider, + this.datastoreMappingContext)); + } + return spy; + } + private GqlDatastoreQuery createQuery( - String gql, boolean isPageQuery, boolean isSliceQuery) { - GqlDatastoreQuery spy = - spy( - new GqlDatastoreQuery<>( - Trade.class, - this.queryMethod, - this.datastoreTemplate, - gql, - this.evaluationContextProvider, - this.datastoreMappingContext)); + String gql, boolean isPageQuery, boolean isSliceQuery, boolean useValueExpressionDelegate) { + GqlDatastoreQuery spy = createQuerySpy(gql, isPageQuery, isSliceQuery, useValueExpressionDelegate); doReturn(isPageQuery).when(spy).isPageQuery(); doReturn(isSliceQuery).when(spy).isSliceQuery(); return spy; } - @Test - void compoundNameConventionTest() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void compoundNameConventionTest(boolean useValueExpressionDelegate) { String gql = "SELECT * FROM " @@ -166,6 +195,7 @@ void compoundNameConventionTest() { doReturn(key).when(this.datastoreTemplate).getKey(any()); + // to be used when using a query method evaluation context EvaluationContext evaluationContext = new StandardEvaluationContext(); for (int i = 0; i < paramVals.length; i++) { evaluationContext.setVariable(paramNames[i], paramVals[i]); @@ -175,7 +205,10 @@ void compoundNameConventionTest() { when(this.evaluationContextProvider.getEvaluationContext(any(), any(), any())) .thenReturn(evaluationContext); - GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, false, false); + // to be used when testing with a value expression delegate + this.valueExpressionDelegate = ValueExpressionDelegate.create(); + + GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, false, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -227,8 +260,9 @@ void compoundNameConventionTest() { verify(this.datastoreTemplate, times(1)).queryKeysOrEntities(any(), eq(Trade.class)); } - @Test - void pageableTest() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void pageableTest(boolean useValueExpressionDelegate) { String gql = "SELECT * FROM trades WHERE price=@price"; @@ -241,7 +275,7 @@ void pageableTest() { when(parameters.hasPageableParameter()).thenReturn(true); when(parameters.getPageableIndex()).thenReturn(1); - GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, false, false); + GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, false, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -268,8 +302,9 @@ void pageableTest() { verify(this.datastoreTemplate, times(1)).queryKeysOrEntities(any(), eq(Trade.class)); } - @Test - void pageableTestSort() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void pageableTestSort(boolean useValueExpressionDelegate) { String gql = "SELECT * FROM trades WHERE price=@price"; @@ -282,7 +317,7 @@ void pageableTestSort() { when(parameters.hasSortParameter()).thenReturn(true); when(parameters.getSortIndex()).thenReturn(1); - GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, false, false); + GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, false, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -307,8 +342,9 @@ void pageableTestSort() { verify(this.datastoreTemplate, times(1)).queryKeysOrEntities(any(), eq(Trade.class)); } - @Test - void pageableTestSlice() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void pageableTestSlice(boolean useValueExpressionDelegate) { String gql = "SELECT * FROM trades WHERE price=@price"; @@ -322,7 +358,7 @@ void pageableTestSlice() { when(parameters.hasPageableParameter()).thenReturn(true); when(parameters.getPageableIndex()).thenReturn(1); - GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, false, true); + GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, false, true, useValueExpressionDelegate); Cursor cursor = Cursor.copyFrom("abc".getBytes()); List params = new ArrayList<>(); @@ -362,8 +398,9 @@ void pageableTestSlice() { assertThat(params.get(1)).containsEntry("offset", cursor); } - @Test - void pageableTestPage() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void pageableTestPage(boolean useValueExpressionDelegate) { String gql = "SELECT * FROM trades WHERE price=@price"; String expected = "SELECT * FROM trades WHERE price=@price LIMIT @limit OFFSET @offset"; @@ -378,7 +415,7 @@ void pageableTestPage() { when(parameters.hasPageableParameter()).thenReturn(true); when(parameters.getPageableIndex()).thenReturn(1); - GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, true, true); + GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, true, true, useValueExpressionDelegate); Cursor cursor = Cursor.copyFrom("abc".getBytes()); @@ -423,8 +460,9 @@ void pageableTestPage() { verify(this.datastoreTemplate, times(2)).queryKeysOrEntities(any(), eq(Trade.class)); } - @Test - void pageableTestPageCursor() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void pageableTestPageCursor(boolean useValueExpressionDelegate) { String gql = "SELECT * FROM trades WHERE price=@price"; String expected = "SELECT * FROM trades WHERE price=@price LIMIT @limit OFFSET @offset"; @@ -443,7 +481,7 @@ void pageableTestPageCursor() { when(parameters.hasPageableParameter()).thenReturn(true); when(parameters.getPageableIndex()).thenReturn(1); - GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, true, true); + GqlDatastoreQuery gqlDatastoreQuery = createQuery(gql, true, true, useValueExpressionDelegate); Cursor cursor = Cursor.copyFrom("abc".getBytes()); @@ -479,8 +517,9 @@ void pageableTestPageCursor() { verify(this.datastoreTemplate, times(1)).queryKeysOrEntities(any(), eq(Trade.class)); } - @Test - void streamResultTest() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void streamResultTest(boolean useValueExpressionDelegate) { Mockito.when(this.queryMethod.getReturnedObjectType()).thenReturn(Trade.class); Parameters parameters = mock(Parameters.class); when(this.queryMethod.getParameters()).thenReturn(parameters); @@ -504,7 +543,7 @@ void streamResultTest() { .when(this.datastoreTemplate) .queryKeysOrEntities(any(), eq(Trade.class)); - GqlDatastoreQuery gqlDatastoreQuery = createQuery("unusedGqlString", false, false); + GqlDatastoreQuery gqlDatastoreQuery = createQuery("unusedGqlString", false, false, useValueExpressionDelegate); Object result = gqlDatastoreQuery.execute(new Parameters[0]); assertThat(result).isInstanceOf(Stream.class); @@ -516,21 +555,32 @@ private Parameters buildParameters(Object[] params, String[] paramNames) { Mockito.when(this.queryMethod.getParameters()).thenReturn(parameters); - when(parameters.getNumberOfParameters()).thenReturn(paramNames.length); - when(parameters.getParameter(anyInt())) - .thenAnswer( - invocation -> { - int index = invocation.getArgument(0); - Parameter param = mock(Parameter.class); - when(param.getName()) - .thenReturn( - paramNames[index] == null - ? Optional.empty() - : Optional.of(paramNames[index])); + final List parameterList = new ArrayList<>(); + for (int index = 0; index < params.length; index++) { - Mockito.when(param.getType()).thenReturn(params[index].getClass()); + Parameter param = mock(Parameter.class); + parameterList.add(param); + when(param.getName()) + .thenReturn( + paramNames[index] == null ? Optional.empty() : Optional.of(paramNames[index])); - return param; + Mockito.when(param.getType()).thenReturn(params[index].getClass()); + when(param.isNamedParameter()).thenReturn(true); + when(param.getRequiredName()).thenReturn(paramNames[index]); + when(param.getIndex()).thenReturn(index); + + when(parameters.getParameter(eq(index))).thenAnswer(invocation -> param); + } + when(parameters.getNumberOfParameters()).thenReturn(paramNames.length); + // we return a new iterator each time. This is because foreach loops call iterable.iterator() + // once per foreach loop. + when(parameters.iterator()) + .thenAnswer( + new Answer() { + @Override + public Iterator answer(InvocationOnMock invocation) throws Throwable { + return parameterList.iterator(); + } }); return parameters; } diff --git a/spring-cloud-gcp-data-datastore/src/test/resources/index.yaml b/spring-cloud-gcp-data-datastore/src/test/resources/index.yaml index c7edc3c5ae..7165b73e09 100644 --- a/spring-cloud-gcp-data-datastore/src/test/resources/index.yaml +++ b/spring-cloud-gcp-data-datastore/src/test/resources/index.yaml @@ -1,3 +1,4 @@ +# For running the ITs in local setups: `cd` into this folder and run `gcloud datastore indexes create index.yaml` indexes: - kind: test_entities_ci diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SpannerQueryLookupStrategy.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SpannerQueryLookupStrategy.java index ab5789c264..1b063da017 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SpannerQueryLookupStrategy.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SpannerQueryLookupStrategy.java @@ -26,6 +26,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -41,10 +42,34 @@ public class SpannerQueryLookupStrategy implements QueryLookupStrategy { private final SpannerMappingContext spannerMappingContext; + private final ValueExpressionDelegate valueExpressionDelegate; + + private final SpelExpressionParser expressionParser; + private QueryMethodEvaluationContextProvider evaluationContextProvider; - private SpelExpressionParser expressionParser; + public SpannerQueryLookupStrategy( + SpannerMappingContext spannerMappingContext, + SpannerTemplate spannerTemplate, + ValueExpressionDelegate valueExpressionDelegate, + SpelExpressionParser expressionParser) { + Assert.notNull(spannerMappingContext, "A valid SpannerMappingContext is required."); + Assert.notNull(spannerTemplate, "A valid SpannerTemplate is required."); + Assert.notNull(valueExpressionDelegate, "A valid ValueExpressionDelegate is required."); + Assert.notNull(expressionParser, "A valid SpelExpressionParser is required."); + this.spannerMappingContext = spannerMappingContext; + this.valueExpressionDelegate = valueExpressionDelegate; + this.evaluationContextProvider = null; + this.spannerTemplate = spannerTemplate; + this.expressionParser = expressionParser; + } + /** + * @deprecated Use {@link + * SpannerQueryLookupStrategy#SpannerQueryLookupStrategy(SpannerMappingContext, + * SpannerTemplate, ValueExpressionDelegate, SpelExpressionParser)} instead. + */ + @Deprecated public SpannerQueryLookupStrategy( SpannerMappingContext spannerMappingContext, SpannerTemplate spannerTemplate, @@ -55,6 +80,7 @@ public SpannerQueryLookupStrategy( Assert.notNull(evaluationContextProvider, "A valid EvaluationContextProvider is required."); Assert.notNull(expressionParser, "A valid SpelExpressionParser is required."); this.spannerMappingContext = spannerMappingContext; + this.valueExpressionDelegate = null; this.evaluationContextProvider = evaluationContextProvider; this.spannerTemplate = spannerTemplate; this.expressionParser = expressionParser; @@ -92,6 +118,17 @@ public RepositoryQuery resolveQuery( SqlSpannerQuery createSqlSpannerQuery( Class entityType, SpannerQueryMethod queryMethod, String sql, boolean isDml) { + if (this.valueExpressionDelegate != null) { + return new SqlSpannerQuery<>( + entityType, + queryMethod, + this.spannerTemplate, + sql, + this.valueExpressionDelegate, + this.expressionParser, + this.spannerMappingContext, + isDml); + } return new SqlSpannerQuery<>( entityType, queryMethod, diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java index b8d66cbaa7..4e1c21c847 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java @@ -47,6 +47,7 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; @@ -80,10 +81,35 @@ public class SqlSpannerQuery extends AbstractSpannerQuery { return builder.build(); }; - private QueryMethodEvaluationContextProvider evaluationContextProvider; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + private final ValueExpressionDelegate valueExpressionDelegate; private SpelExpressionParser expressionParser; + SqlSpannerQuery( + Class type, + SpannerQueryMethod queryMethod, + SpannerTemplate spannerTemplate, + String sql, + ValueExpressionDelegate valueExpressionDelegate, + SpelExpressionParser expressionParser, + SpannerMappingContext spannerMappingContext, + boolean isDml) { + super(type, queryMethod, spannerTemplate, spannerMappingContext); + this.evaluationContextProvider = null; + this.valueExpressionDelegate = valueExpressionDelegate; + this.expressionParser = expressionParser; + this.sql = StringUtils.trimTrailingCharacter(sql.trim(), ';'); + this.isDml = isDml; + } + + /** + * @deprecated Use {@link + * SpannerQueryLookupStrategy#SpannerQueryLookupStrategy(SpannerMappingContext, + * SpannerTemplate, ValueExpressionDelegate, SpelExpressionParser)} instead. + */ + @Deprecated SqlSpannerQuery( Class type, SpannerQueryMethod queryMethod, @@ -95,6 +121,7 @@ public class SqlSpannerQuery extends AbstractSpannerQuery { boolean isDml) { super(type, queryMethod, spannerTemplate, spannerMappingContext); this.evaluationContextProvider = evaluationContextProvider; + this.valueExpressionDelegate = null; this.expressionParser = expressionParser; this.sql = StringUtils.trimTrailingCharacter(sql.trim(), ';'); this.isDml = isDml; @@ -153,14 +180,21 @@ private static String resolveEntityClassNames( return result; } + private EvaluationContext getEvaluationContext(QueryTagValue queryTagValue) { + if (evaluationContextProvider != null) { + return this.evaluationContextProvider.getEvaluationContext( + this.queryMethod.getParameters(), queryTagValue.rawParams); + } + return this.valueExpressionDelegate.getEvaluationContextAccessor().create(this.queryMethod.getParameters()) + .getEvaluationContext(queryTagValue.rawParams).getEvaluationContext(); + } + private void resolveSpelTags(QueryTagValue queryTagValue) { Expression[] expressions = detectExpressions(queryTagValue.sql); StringBuilder sb = new StringBuilder(); Map valueToTag = new HashMap<>(); int tagNum = 0; - EvaluationContext evaluationContext = - this.evaluationContextProvider.getEvaluationContext( - this.queryMethod.getParameters(), queryTagValue.rawParams); + EvaluationContext evaluationContext = getEvaluationContext(queryTagValue); for (Expression expression : expressions) { if (expression instanceof LiteralExpression) { sb.append(expression.getValue(String.class)); diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/support/SpannerRepositoryFactory.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/support/SpannerRepositoryFactory.java index 2e3dbf7d8a..041e542671 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/support/SpannerRepositoryFactory.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/support/SpannerRepositoryFactory.java @@ -36,6 +36,8 @@ import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -100,6 +102,22 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { } @Override + protected Optional getQueryLookupStrategy( + @Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { + + return Optional.of( + new SpannerQueryLookupStrategy( + this.spannerMappingContext, + this.spannerTemplate, + valueExpressionDelegate, + EXPRESSION_PARSER)); + } + + /** + * @deprecated Use {@link #getQueryLookupStrategy(Key, ValueExpressionDelegate)} instead. + */ + @Override + @Deprecated protected Optional getQueryLookupStrategy( @Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { @@ -141,6 +159,11 @@ private QueryMethodEvaluationContextProvider delegateContextProvider( new BeanFactoryResolver(SpannerRepositoryFactory.this.applicationContext)); return evaluationContext; } + + @Override + public EvaluationContextProvider getEvaluationContextProvider() { + return (EvaluationContextProvider) evaluationContextProvider; + } }; } diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentPropertyImplTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentPropertyImplTests.java index 3dcdcddf14..7421891218 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentPropertyImplTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentPropertyImplTests.java @@ -25,7 +25,6 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.test.context.junit.jupiter.SpringExtension; diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SpannerQueryLookupStrategyTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SpannerQueryLookupStrategyTests.java index 1d1745819e..7db35d5066 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SpannerQueryLookupStrategyTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SpannerQueryLookupStrategyTests.java @@ -44,12 +44,14 @@ import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.spel.standard.SpelExpressionParser; /** Tests Spanner Query Method lookups. */ @@ -63,6 +65,8 @@ class SpannerQueryLookupStrategyTests { private SpannerQueryLookupStrategy spannerQueryLookupStrategy; + private ValueExpressionDelegate valueExpressionDelegate; + private QueryMethodEvaluationContextProvider evaluationContextProvider; private SpelExpressionParser spelExpressionParser; @@ -73,9 +77,9 @@ void initMocks() { this.spannerMappingContext = new SpannerMappingContext(); this.spannerTemplate = mock(SpannerTemplate.class); this.queryMethod = mock(SpannerQueryMethod.class); + this.valueExpressionDelegate = mock(ValueExpressionDelegate.class); this.evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); this.spelExpressionParser = new SpelExpressionParser(); - this.spannerQueryLookupStrategy = getSpannerQueryLookupStrategy(); when(this.queryMethod.getQueryAnnotation()) .thenReturn( @@ -97,8 +101,10 @@ public boolean dmlStatement() { }); } - @Test - void resolveSqlQueryTest() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void resolveSqlQueryTest(boolean useValueExpressionDelegate) { + this.spannerQueryLookupStrategy = getSpannerQueryLookupStrategy(useValueExpressionDelegate); String queryName = "fakeNamedQueryName"; String query = "fake query"; when(this.queryMethod.getNamedQueryName()).thenReturn(queryName); @@ -131,8 +137,10 @@ void resolveSqlQueryTest() { .createSqlSpannerQuery(eq(Object.class), same(this.queryMethod), eq(query), eq(false)); } - @Test - void resolvePartTreeQueryTest() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void resolvePartTreeQueryTest(boolean useValueExpressionDelegate) { + this.spannerQueryLookupStrategy = getSpannerQueryLookupStrategy(useValueExpressionDelegate); String queryName = "fakeNamedQueryName"; when(this.queryMethod.getNamedQueryName()).thenReturn(queryName); NamedQueries namedQueries = mock(NamedQueries.class); @@ -144,14 +152,25 @@ void resolvePartTreeQueryTest() { .createPartTreeSpannerQuery(eq(Object.class), same(this.queryMethod)); } - private SpannerQueryLookupStrategy getSpannerQueryLookupStrategy() { - SpannerQueryLookupStrategy spannerQueryLookupStrategy = - spy( - new SpannerQueryLookupStrategy( - this.spannerMappingContext, - this.spannerTemplate, - this.evaluationContextProvider, - this.spelExpressionParser)); + private SpannerQueryLookupStrategy getSpannerQueryLookupStrategy(boolean useValueExpressionDelegate) { + SpannerQueryLookupStrategy spannerQueryLookupStrategy; + if (useValueExpressionDelegate) { + spannerQueryLookupStrategy = + spy( + new SpannerQueryLookupStrategy( + this.spannerMappingContext, + this.spannerTemplate, + this.valueExpressionDelegate, + this.spelExpressionParser)); + } else { + spannerQueryLookupStrategy = + spy( + new SpannerQueryLookupStrategy( + this.spannerMappingContext, + this.spannerTemplate, + this.evaluationContextProvider, + this.spelExpressionParser)); + } doReturn(Object.class).when(spannerQueryLookupStrategy).getEntityType(any()); doReturn(null).when(spannerQueryLookupStrategy).createPartTreeSpannerQuery(any(), any()); doReturn(this.queryMethod) @@ -160,8 +179,10 @@ private SpannerQueryLookupStrategy getSpannerQueryLookupStrategy() { return spannerQueryLookupStrategy; } - @Test - void getChildrenRowsQueryTest() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void getChildrenRowsQueryTest(boolean useValueExpressionDelegate) { + this.spannerQueryLookupStrategy = getSpannerQueryLookupStrategy(useValueExpressionDelegate); TestEntity t = new TestEntity(); t.id = "key"; t.id2 = "key2"; @@ -182,8 +203,10 @@ void getChildrenRowsQueryTest() { assertThat(statement.getParameters().get("tag1").getString()).isEqualTo("key2"); } - @Test - void getColumnsStringForSelectTest() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void getColumnsStringForSelectTest(boolean useValueExpressionDelegate) { + this.spannerQueryLookupStrategy = getSpannerQueryLookupStrategy(useValueExpressionDelegate); TestEntity t = new TestEntity(); t.id = "key"; t.id2 = "key2"; @@ -201,9 +224,11 @@ void getColumnsStringForSelectTest() { + " childEntities"); } - @Test + @ParameterizedTest @SuppressWarnings("unchecked") - void getColumnsStringForSelectMultipleTest() { + @ValueSource(booleans = {true, false}) + void getColumnsStringForSelectMultipleTest(boolean useValueExpressionDelegate) { + this.spannerQueryLookupStrategy = getSpannerQueryLookupStrategy(useValueExpressionDelegate); final SpannerPersistentEntity entity = (SpannerPersistentEntity) this.spannerMappingContext.getPersistentEntity(TestEntity.class); diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQueryTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQueryTests.java index 9adf5273d7..727619fe87 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQueryTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQueryTests.java @@ -59,20 +59,25 @@ import java.util.function.Function; import org.assertj.core.data.Offset; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; +import org.springframework.data.expression.ValueEvaluationContext; +import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.repository.query.DefaultParameters; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -86,12 +91,16 @@ class SqlSpannerQueryTests { private SpannerQueryMethod queryMethod; + private ValueExpressionDelegate valueExpressionDelegate; + private QueryMethodEvaluationContextProvider evaluationContextProvider; private SpelExpressionParser expressionParser; private SpannerMappingContext spannerMappingContext = new SpannerMappingContext(new Gson()); + private ValueEvaluationContext valueEvaluationContext; + private final Sort sort = Sort.by(Order.asc("COLA"), Order.desc("COLB")); private final Pageable pageable = PageRequest.of(3, 10, this.sort); @@ -100,6 +109,7 @@ class SqlSpannerQueryTests { private final DatabaseClient databaseClient = mock(DatabaseClient.class); + @BeforeEach void initMocks() throws NoSuchMethodException { this.queryMethod = mock(SpannerQueryMethod.class); @@ -119,9 +129,33 @@ void initMocks() throws NoSuchMethodException { this.spannerMappingContext, this.spannerEntityProcessor, true))); this.expressionParser = new SpelExpressionParser(); this.evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); + + this.valueExpressionDelegate = mock(ValueExpressionDelegate.class); + QueryMethodValueEvaluationContextAccessor evaluationContextAccessor = mock(QueryMethodValueEvaluationContextAccessor.class); + ValueEvaluationContextProvider evaluationContextProvider = + mock(ValueEvaluationContextProvider.class); + this.valueEvaluationContext = mock(ValueEvaluationContext.class); + when(this.valueExpressionDelegate.getEvaluationContextAccessor()) + .thenReturn(evaluationContextAccessor); + when(evaluationContextAccessor.create(any())).thenReturn(evaluationContextProvider); + when(evaluationContextProvider.getEvaluationContext(any())).thenReturn(valueEvaluationContext); + when(valueEvaluationContext.getEvaluationContext()).thenReturn(mock(EvaluationContext.class)); + } - private SqlSpannerQuery createQuery(String sql, Class theClass, boolean isDml) { + @SuppressWarnings("deprecation") + private SqlSpannerQuery createQuery(String sql, Class theClass, boolean isDml, boolean useValueExpressionDelegate) { + if (useValueExpressionDelegate) { + return new SqlSpannerQuery( + theClass, + this.queryMethod, + this.spannerTemplate, + sql, + this.valueExpressionDelegate, + this.expressionParser, + this.spannerMappingContext, + isDml); + } return new SqlSpannerQuery( theClass, this.queryMethod, @@ -133,8 +167,9 @@ private SqlSpannerQuery createQuery(String sql, Class theClass, boolea isDml); } - @Test - void noPageableParamQueryTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void noPageableParamQueryTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "SELECT DISTINCT * FROM " + ":com.google.cloud.spring.data.spanner.repository.query.SqlSpannerQueryTests$Trade:"; @@ -155,7 +190,7 @@ void noPageableParamQueryTest() throws NoSuchMethodException { when(this.evaluationContextProvider.getEvaluationContext(any(), any())) .thenReturn(evaluationContext); - SqlSpannerQuery sqlSpannerQuery = createQuery(sql, toReturn, false); + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, toReturn, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -184,8 +219,9 @@ void noPageableParamQueryTest() throws NoSuchMethodException { .query(eq(Trade.class), any(Statement.class), any(SpannerQueryOptions.class)); } - @Test - void pageableParamQueryTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void pageableParamQueryTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "SELECT * FROM" @@ -214,7 +250,7 @@ void pageableParamQueryTest() throws NoSuchMethodException { when(this.evaluationContextProvider.getEvaluationContext(any(), any())) .thenReturn(evaluationContext); - SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Child.class, false); + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Child.class, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -248,8 +284,9 @@ void pageableParamQueryTest() throws NoSuchMethodException { verify(this.spannerTemplate, times(1)).executeQuery(any(), any()); } - @Test - void sortParamQueryTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sortParamQueryTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "SELECT * FROM" @@ -277,7 +314,7 @@ void sortParamQueryTest() throws NoSuchMethodException { when(this.evaluationContextProvider.getEvaluationContext(any(), any())) .thenReturn(evaluationContext); - SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Child.class, false); + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Child.class, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -311,8 +348,9 @@ void sortParamQueryTest() throws NoSuchMethodException { verify(this.spannerTemplate, times(1)).executeQuery(any(), any()); } - @Test - void sortAndPageableQueryTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sortAndPageableQueryTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "SELECT * FROM" @@ -341,7 +379,7 @@ void sortAndPageableQueryTest() throws NoSuchMethodException { when(this.evaluationContextProvider.getEvaluationContext(any(), any())) .thenReturn(evaluationContext); - SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Child.class, false); + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Child.class, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -375,8 +413,9 @@ void sortAndPageableQueryTest() throws NoSuchMethodException { verify(this.spannerTemplate, times(1)).executeQuery(any(), any()); } - @Test - void compoundNameConventionTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void compoundNameConventionTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "SELECT DISTINCT * FROM " @@ -444,10 +483,11 @@ void compoundNameConventionTest() throws NoSuchMethodException { for (int i = 0; i < params.length; i++) { evaluationContext.setVariable(paramNames[i], params[i]); } + when(valueEvaluationContext.getEvaluationContext()).thenReturn(evaluationContext); when(this.evaluationContextProvider.getEvaluationContext(any(), any())) .thenReturn(evaluationContext); - SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Trade.class, false); + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Trade.class, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -506,14 +546,17 @@ void compoundNameConventionTest() throws NoSuchMethodException { verify(this.spannerTemplate, times(1)).executeQuery(any(), any()); } - @Test - void dmlTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void dmlTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "dml statement here"; TransactionContext context = mock(TransactionContext.class); TransactionRunner transactionRunner = mock(TransactionRunner.class); when(this.databaseClient.readWriteTransaction()).thenReturn(transactionRunner); + when(valueEvaluationContext.getEvaluationContext()).thenReturn(mock(EvaluationContext.class)); + when(transactionRunner.run(any())) .thenAnswer( invocation -> { @@ -526,7 +569,7 @@ void dmlTest() throws NoSuchMethodException { Mockito.when(this.queryMethod.getParameters()) .thenReturn(new DefaultParameters(ParametersSource.of(method))); - SqlSpannerQuery sqlSpannerQuery = spy(createQuery(sql, Trade.class, true)); + SqlSpannerQuery sqlSpannerQuery = spy(createQuery(sql, Trade.class, true, useValueExpressionDelegate)); doReturn(long.class).when(sqlSpannerQuery).getReturnedSimpleConvertableItemType(); doReturn(null).when(sqlSpannerQuery).convertToSimpleReturnType(any(), any()); @@ -536,8 +579,9 @@ void dmlTest() throws NoSuchMethodException { verify(this.spannerTemplate, times(1)).executeDmlStatement(any()); } - @Test - void sqlCountWithWhereTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sqlCountWithWhereTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "SELECT count(1) FROM" + " :com.google.cloud.spring.data.spanner.repository.query.SqlSpannerQueryTests$Child:" @@ -552,14 +596,7 @@ void sqlCountWithWhereTest() throws NoSuchMethodException { when(queryMethod.isCollectionQuery()).thenReturn(false); when(queryMethod.getReturnedObjectType()).thenReturn((Class) long.class); - EvaluationContext evaluationContext = new StandardEvaluationContext(); - for (int i = 0; i < params.length; i++) { - evaluationContext.setVariable(paramNames[i], params[i]); - } - when(this.evaluationContextProvider.getEvaluationContext(any(), any())) - .thenReturn(evaluationContext); - - SqlSpannerQuery sqlSpannerQuery = createQuery(sql, long.class, false); + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, long.class, false, useValueExpressionDelegate); Struct row = mock(Struct.class); when(row.getType()) @@ -603,8 +640,9 @@ void sqlCountWithWhereTest() throws NoSuchMethodException { verify(this.spannerTemplate).executeQuery(any(), any()); } - @Test - void sqlReturnTypeIsJsonFieldTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sqlReturnTypeIsJsonFieldTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "SELECT details from singer where stageName = @stageName"; Object[] params = new Object[] {"STAGENAME"}; @@ -617,13 +655,7 @@ void sqlReturnTypeIsJsonFieldTest() throws NoSuchMethodException { when(resultProcessor.getReturnedType()).thenReturn(returnedType); when(returnedType.getReturnedType()).thenReturn((Class) Detail.class); - EvaluationContext evaluationContext = new StandardEvaluationContext(); - - evaluationContext.setVariable(paramNames[0], params[0]); - when(this.evaluationContextProvider.getEvaluationContext(any(), any())) - .thenReturn(evaluationContext); - - SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Singer.class, false); + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Singer.class, false, useValueExpressionDelegate); doAnswer( invocation -> { @@ -666,12 +698,13 @@ void sqlReturnTypeIsJsonFieldTest() throws NoSuchMethodException { assertThat(((Detail) result).p2).isEqualTo("5"); } - @Test - void sqlReturnTypeIsArrayJsonFieldTest() throws NoSuchMethodException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sqlReturnTypeIsArrayJsonFieldTest(boolean useValueExpressionDelegate) throws NoSuchMethodException { String sql = "SELECT detailsList from singer where stageName = @stageName"; - Object[] params = new Object[]{"STAGENAME"}; - String[] paramNames = new String[]{"stageName"}; + Object[] params = new Object[] {"STAGENAME"}; + String[] paramNames = new String[] {"stageName"}; when(queryMethod.isCollectionQuery()).thenReturn(true); ResultProcessor resultProcessor = mock(ResultProcessor.class); @@ -680,23 +713,17 @@ void sqlReturnTypeIsArrayJsonFieldTest() throws NoSuchMethodException { when(resultProcessor.getReturnedType()).thenReturn(returnedType); when(returnedType.getReturnedType()).thenReturn((Class) Detail.class); - EvaluationContext evaluationContext = new StandardEvaluationContext(); - - evaluationContext.setVariable(paramNames[0], params[0]); - when(this.evaluationContextProvider.getEvaluationContext(any(), any())) - .thenReturn(evaluationContext); - - SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Singer.class, false); + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Singer.class, false, useValueExpressionDelegate); doAnswer( - invocation -> { - Statement statement = invocation.getArgument(1); - assertThat(statement.getSql()).isEqualTo(sql); - Map paramMap = statement.getParameters(); - assertThat(paramMap.get("stageName").getString()).isEqualTo(params[0]); - - return null; - }) + invocation -> { + Statement statement = invocation.getArgument(1); + assertThat(statement.getSql()).isEqualTo(sql); + Map paramMap = statement.getParameters(); + assertThat(paramMap.get("stageName").getString()).isEqualTo(params[0]); + + return null; + }) .when(this.spannerTemplate) .query((Function) any(), any(), any()); @@ -717,17 +744,22 @@ void sqlReturnTypeIsArrayJsonFieldTest() throws NoSuchMethodException { Struct row = mock(Struct.class); when(row.getType()) - .thenReturn(Type.struct( - Arrays.asList(Type.StructField.of("detailsList", Type.array(Type.json()))))); + .thenReturn( + Type.struct( + Arrays.asList(Type.StructField.of("detailsList", Type.array(Type.json()))))); when(row.getColumnType(0)).thenReturn(Type.array(Type.json())); - when(row.getJsonList(0)).thenReturn(Arrays.asList("{\"p1\":\"address line\",\"p2\":\"5\"}", - "{\"p1\":\"address line 2\",\"p2\":\"6\"}")); + when(row.getJsonList(0)) + .thenReturn( + Arrays.asList( + "{\"p1\":\"address line\",\"p2\":\"5\"}", + "{\"p1\":\"address line 2\",\"p2\":\"6\"}")); when(row.getColumnType("detailsList")).thenReturn(Type.array(Type.json())); Object result = rowFunc.apply(row); assertThat(result).isInstanceOf(List.class); - assertThat((List) result).hasSize(2) + assertThat((List) result) + .hasSize(2) .containsExactly(new Detail("address line", "5"), new Detail("address line 2", "6")); } @@ -762,8 +794,7 @@ public boolean equals(Object o) { return false; } Detail detail = (Detail) o; - return Objects.equal(p1, detail.p1) - && Objects.equal(p2, detail.p2); + return Objects.equal(p1, detail.p1) && Objects.equal(p2, detail.p2); } @Override diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/support/SpannerRepositoryFactoryTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/support/SpannerRepositoryFactoryTests.java index bcf962dc68..70c7624e30 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/support/SpannerRepositoryFactoryTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/support/SpannerRepositoryFactoryTests.java @@ -36,6 +36,7 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** Tests for the Spanner repository factory. */ class SpannerRepositoryFactoryTests { @@ -99,6 +100,10 @@ void getRepositoryBaseClassTest() { @Test void getQueryLookupStrategyTest() { Optional qls = + this.spannerRepositoryFactory.getQueryLookupStrategy( + null, mock(ValueExpressionDelegate.class)); + assertThat(qls.get()).isInstanceOf(SpannerQueryLookupStrategy.class); + qls = this.spannerRepositoryFactory.getQueryLookupStrategy( null, mock(QueryMethodEvaluationContextProvider.class)); assertThat(qls.get()).isInstanceOf(SpannerQueryLookupStrategy.class); diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/AbstractSpannerIntegrationTest.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/AbstractSpannerIntegrationTest.java index 51b38f59a2..d420685210 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/AbstractSpannerIntegrationTest.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/AbstractSpannerIntegrationTest.java @@ -17,7 +17,6 @@ package com.google.cloud.spring.data.spanner.test; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; import com.google.cloud.spring.data.spanner.core.SpannerOperations; import com.google.cloud.spring.data.spanner.core.admin.SpannerDatabaseAdminTemplate; diff --git a/spring-cloud-gcp-pubsub-stream-binder/src/test/java/com/google/cloud/spring/stream/binder/pubsub/PubSubEmulator.java b/spring-cloud-gcp-pubsub-stream-binder/src/test/java/com/google/cloud/spring/stream/binder/pubsub/PubSubEmulator.java index f642466980..4f82db1951 100644 --- a/spring-cloud-gcp-pubsub-stream-binder/src/test/java/com/google/cloud/spring/stream/binder/pubsub/PubSubEmulator.java +++ b/spring-cloud-gcp-pubsub-stream-binder/src/test/java/com/google/cloud/spring/stream/binder/pubsub/PubSubEmulator.java @@ -36,6 +36,7 @@ import java.util.StringTokenizer; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.lang3.SystemUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.extension.AfterAllCallback; @@ -55,9 +56,7 @@ */ public class PubSubEmulator implements BeforeAllCallback, AfterAllCallback, ParameterResolver { - private static final Path EMULATOR_CONFIG_DIR = - Paths.get(System.getProperty("user.home")) - .resolve(Paths.get(".config", "gcloud", "emulators", "pubsub")); + private static final Path EMULATOR_CONFIG_DIR = getEmulatorConfigDir(); private static final String ENV_FILE_NAME = "env.yaml"; @@ -198,13 +197,13 @@ private void startEmulator() throws IOException, InterruptedException { try { this.emulatorProcess = - new ProcessBuilder("gcloud", "beta", "emulators", "pubsub", "start").start(); + new ProcessBuilder("bash", "-c", "gcloud beta emulators pubsub start").start(); } catch (IOException ex) { fail("Gcloud not found; leaving host/port uninitialized."); } if (configPresent) { - updateConfig(watchService); + waitForConfigUpdates(watchService); watchService.close(); } else { createConfig(); @@ -240,7 +239,7 @@ private void determineHostPort() throws IOException, InterruptedException { private void createConfig() { await() .pollInterval(Duration.ofSeconds(1)) - .atMost(Duration.ofSeconds(5)) + .atMost(Duration.ofSeconds(10)) .untilAsserted(() -> assertThat(EMULATOR_CONFIG_PATH.toFile()).exists()); } @@ -252,7 +251,7 @@ private void createConfig() { * @throws InterruptedException which should interrupt the peaceful slumber and bubble up to fail * the test. */ - private void updateConfig(WatchService watchService) throws InterruptedException { + private void waitForConfigUpdates(WatchService watchService) throws InterruptedException { int attempts = 10; while (--attempts >= 0) { WatchKey key = watchService.poll(1000, TimeUnit.MILLISECONDS); @@ -285,4 +284,13 @@ private void killProcess(String pid) { LOGGER.warn("Failed to clean up PID " + pid); } } + + private static Path getEmulatorConfigDir() { + if (SystemUtils.IS_OS_WINDOWS) { + return Paths.get(System.getenv("APPDATA")) + .resolve(Paths.get("gcloud", "emulators", "pubsub")); + } + return Paths.get(System.getProperty("user.home")) + .resolve(Paths.get(".config", "gcloud", "emulators", "pubsub")); + } } diff --git a/spring-cloud-gcp-pubsub-stream-binder/src/test/java/com/google/cloud/spring/stream/binder/pubsub/PubSubMessageChannelBinderTests.java b/spring-cloud-gcp-pubsub-stream-binder/src/test/java/com/google/cloud/spring/stream/binder/pubsub/PubSubMessageChannelBinderTests.java index f0f8a882d4..645d128d20 100644 --- a/spring-cloud-gcp-pubsub-stream-binder/src/test/java/com/google/cloud/spring/stream/binder/pubsub/PubSubMessageChannelBinderTests.java +++ b/spring-cloud-gcp-pubsub-stream-binder/src/test/java/com/google/cloud/spring/stream/binder/pubsub/PubSubMessageChannelBinderTests.java @@ -17,13 +17,10 @@ package com.google.cloud.spring.stream.binder.pubsub; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.api.gax.core.CredentialsProvider; -import com.google.auth.CredentialTypeForMetrics; -import com.google.auth.Credentials; import com.google.cloud.spring.core.GcpProjectIdProvider; import com.google.cloud.spring.pubsub.PubSubAdmin; import com.google.cloud.spring.pubsub.core.PubSubTemplate; @@ -104,10 +101,11 @@ void init() { new ApplicationContextRunner() .withBean(PubSubTemplate.class, () -> pubSubTemplate) .withBean(PubSubAdmin.class, () -> pubSubAdmin) + .withUserConfiguration(BaseTestConfiguration.class) .withConfiguration( AutoConfigurations.of( - PubSubBinderConfiguration.class, - PubSubExtendedBindingProperties.class)); + PubSubBinderConfiguration.class + )); this.binder = new PubSubMessageChannelBinder(new String[0], this.channelProvisioner, this.pubSubTemplate, this.properties); } @@ -364,6 +362,24 @@ void testConsumerEndpointCreationWithHeadersProvided() { }); } + @EnableAutoConfiguration + public static class BaseTestConfiguration { + @Bean + public PubSubExtendedBindingProperties pubSubExtendedBindingProperties() { + return new PubSubExtendedBindingProperties(); + } + + @Bean + public CredentialsProvider googleCredentials() { + return () -> TestUtils.MOCK_CREDENTIALS; + } + + @Bean + public GcpProjectIdProvider projectIdProvider() { + return () -> "fake project"; + } + } + @EnableAutoConfiguration public static class PubSubBinderTestConfig { @@ -387,15 +403,5 @@ public Supplier> producer() { public Consumer consumer() { return str -> LOGGER.info("received " + str); } - - @Bean - public GcpProjectIdProvider projectIdProvider() { - return () -> "fake project"; - } - - @Bean - public CredentialsProvider googleCredentials() { - return () -> TestUtils.MOCK_CREDENTIALS; - } } } diff --git a/spring-cloud-gcp-samples/pom.xml b/spring-cloud-gcp-samples/pom.xml index fe569d5399..2dc0572e26 100644 --- a/spring-cloud-gcp-samples/pom.xml +++ b/spring-cloud-gcp-samples/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.4 + 3.4.2 @@ -52,7 +52,6 @@ - spring-cloud-gcp-config-sample spring-cloud-gcp-trace-sample spring-cloud-gcp-logging-sample spring-cloud-gcp-sql-mysql-sample diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-kotlin-samples/spring-cloud-gcp-kotlin-app-sample/src/main/kotlin/com/example/data/Person.kt b/spring-cloud-gcp-samples/spring-cloud-gcp-kotlin-samples/spring-cloud-gcp-kotlin-app-sample/src/main/kotlin/com/example/data/Person.kt index 3fabed0a32..aa0af5e13a 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-kotlin-samples/spring-cloud-gcp-kotlin-app-sample/src/main/kotlin/com/example/data/Person.kt +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-kotlin-samples/spring-cloud-gcp-kotlin-app-sample/src/main/kotlin/com/example/data/Person.kt @@ -34,5 +34,5 @@ data class Person( val email: String, @Id @GeneratedValue - var id: Long? = 0 + var id: Long? = null ) diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretConfiguration.java b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretConfiguration.java index 01a4058738..bc1f831322 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretConfiguration.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretConfiguration.java @@ -20,15 +20,16 @@ import org.springframework.cloud.context.config.annotation.RefreshScope; @ConfigurationProperties("application") -@RefreshScope public class SecretConfiguration { private String secret; + @RefreshScope public void setSecret(String secret) { this.secret = secret; } + @RefreshScope public String getSecret() { return secret; } diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretManagerWebController.java b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretManagerWebController.java index ed2e1a05d5..6491b67214 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretManagerWebController.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretManagerWebController.java @@ -39,12 +39,27 @@ public class SecretManagerWebController { // secret can be refreshed when decorated with @RefreshScope on the class. private final SecretConfiguration configuration; - // For the default value takes place, there should be no property called `application-fake` + // This syntax is not recommended. Please switch your code to the `sm@my_secret` syntax. Users + // will be warned if using this syntax. + // Note that the colon of the protocol specification section must be escaped; + // See https://github.com/GoogleCloudPlatform/spring-cloud-gcp/issues/3440 + //@Value("${sm\\://application-fake:DEFAULT}") + //private String defaultSecretDeprecatedSyntax; + + // This syntax is not recommended. Please switch your code to the `sm@my_secret` syntax. Users + // will be warned if using this syntax. + //@Value("${sm://application-secret}") + //private String appSecretFromValueDeprecatedSyntax; + + // For the default value to take place, there should be no property called `application-fake` // in property files. - @Value("${${sm://application-fake}:DEFAULT}") + // When using the new syntax, it is not necessary to escape the colon character by nesting + // placeholders as done with the legacy syntax (${${sm://secret}:DEFAULT}). + @Value("${sm@application-fake:DEFAULT}") private String defaultSecret; + // Application secrets can be accessed using @Value syntax. - @Value("${sm://application-secret}") + @Value("${sm@application-secret:DEFAULT}") private String appSecretFromValue; public SecretManagerWebController(SecretManagerTemplate secretManagerTemplate, @@ -77,11 +92,11 @@ public String getSecret( String secretPayload; if (StringUtils.isEmpty(projectId)) { secretPayload = - this.secretManagerTemplate.getSecretString("sm://" + secretId + "/" + version); + this.secretManagerTemplate.getSecretString("sm@" + secretId + "/" + version); } else { secretPayload = this.secretManagerTemplate.getSecretString( - "sm://" + projectId + "/" + secretId + "/" + version); + "sm@" + projectId + "/" + secretId + "/" + version); } return "Secret ID: " diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/resources/application.properties b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/resources/application.properties index a785080cd1..b33fcd96ec 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/resources/application.properties +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/resources/application.properties @@ -11,7 +11,14 @@ management.endpoints.web.exposure.include=refresh # enable external resource from GCP Secret Manager. -spring.config.import=sm:// -application.secret=${sm://application-secret} +# Here we enable the config loader for GCP Secret Manager +# The sm:// syntax has been Deprecated and may be removed in a future version of +# Spring Cloud GCP. Please use the sm@ syntax instead. +#spring.config.import=sm:// +spring.config.import=sm@ +application.secret=${sm@application-secret} # enable default secret value when accessing non-exited secret. spring.cloud.gcp.secretmanager.allow-default-secret=true +# ensure this property is set to false when building a native image. +# see https://docs.spring.io/spring-cloud-config/reference/client.html#aot-and-native-image-support +spring.cloud.refresh.enabled=true diff --git a/spring-cloud-gcp-secretmanager/src/main/java/com/google/cloud/spring/secretmanager/SecretManagerPropertyUtils.java b/spring-cloud-gcp-secretmanager/src/main/java/com/google/cloud/spring/secretmanager/SecretManagerPropertyUtils.java index 5d3c2103fa..f7fc560614 100644 --- a/spring-cloud-gcp-secretmanager/src/main/java/com/google/cloud/spring/secretmanager/SecretManagerPropertyUtils.java +++ b/spring-cloud-gcp-secretmanager/src/main/java/com/google/cloud/spring/secretmanager/SecretManagerPropertyUtils.java @@ -16,24 +16,41 @@ package com.google.cloud.spring.secretmanager; +import static com.google.cloud.spring.secretmanager.SecretManagerSyntaxUtils.getMatchedPrefixes; +import static com.google.cloud.spring.secretmanager.SecretManagerSyntaxUtils.warnIfUsingDeprecatedSyntax; + import com.google.cloud.secretmanager.v1.SecretVersionName; import com.google.cloud.spring.core.GcpProjectIdProvider; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.util.Assert; /** Utilities for parsing Secret Manager properties. */ final class SecretManagerPropertyUtils { - private static final String GCP_SECRET_PREFIX = "sm://"; + private static final Logger logger = LoggerFactory.getLogger(SecretManagerPropertyUtils.class); private SecretManagerPropertyUtils() {} + static SecretVersionName getSecretVersionName( - String input, GcpProjectIdProvider projectIdProvider) { - if (!input.startsWith(GCP_SECRET_PREFIX)) { + final String input, GcpProjectIdProvider projectIdProvider) { + Optional usedPrefix = getMatchedPrefixes(input::startsWith); + + // Since spring-core 6.2.2, the property resolution mechanism will try a full match that + // may include a default string if provided. For example, a @Value("${sm@secret:default}") will + // cause two attempts: one with sm@secret:default as a whole string (we don't want this), + // and one with sm@secret (that's the one we want to process). The colon is also an invalid + // character in secret IDs. + // See https://github.com/spring-projects/spring-framework/issues/34124. + final boolean isAttemptingFullStringMatch = input.replace("sm://", "").contains(":"); + if (usedPrefix.isEmpty() || isAttemptingFullStringMatch) { return null; } + warnIfUsingDeprecatedSyntax(logger, usedPrefix.orElse("")); - String resourcePath = input.substring(GCP_SECRET_PREFIX.length()); + String resourcePath = input.substring(usedPrefix.get().length()); String[] tokens = resourcePath.split("/"); String projectId = projectIdProvider.getProjectId(); @@ -41,26 +58,26 @@ static SecretVersionName getSecretVersionName( String version = "latest"; if (tokens.length == 1) { - // property is form "sm://" + // property is of the form "sm@" secretId = tokens[0]; } else if (tokens.length == 2) { - // property is form "sm:///" + // property is of the form "sm@/" secretId = tokens[0]; version = tokens[1]; } else if (tokens.length == 3) { - // property is form "sm:////" + // property is of the form "sm@//" projectId = tokens[0]; secretId = tokens[1]; version = tokens[2]; } else if (tokens.length == 4 && tokens[0].equals("projects") && tokens[2].equals("secrets")) { - // property is form "sm://projects//secrets/" + // property is of the form "sm@projects//secrets/" projectId = tokens[1]; secretId = tokens[3]; } else if (tokens.length == 6 && tokens[0].equals("projects") && tokens[2].equals("secrets") && tokens[4].equals("versions")) { - // property is form "sm://projects//secrets//versions/" + // property is of the form "sm@projects//secrets//versions/" projectId = tokens[1]; secretId = tokens[3]; version = tokens[5]; diff --git a/spring-cloud-gcp-secretmanager/src/main/java/com/google/cloud/spring/secretmanager/SecretManagerSyntaxUtils.java b/spring-cloud-gcp-secretmanager/src/main/java/com/google/cloud/spring/secretmanager/SecretManagerSyntaxUtils.java new file mode 100644 index 0000000000..68d61956b6 --- /dev/null +++ b/spring-cloud-gcp-secretmanager/src/main/java/com/google/cloud/spring/secretmanager/SecretManagerSyntaxUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spring.secretmanager; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; + +public class SecretManagerSyntaxUtils { + private static final String DEPRECATED_PREFIX = "sm://"; + + private static final String PREFERRED_PREFIX = "sm@"; + + /** + * Prefixes for Google Cloud Secret Manager resources. + */ + public static final List PREFIXES = ImmutableList.of(PREFERRED_PREFIX, DEPRECATED_PREFIX); + + public static Optional getMatchedPrefixes(PrefixMatcher matcher) { + return PREFIXES.stream().filter(matcher::matches).findFirst(); + } + + public static void warnIfUsingDeprecatedSyntax(Logger logger, String value) { + if (!logger.isWarnEnabled() || !value.startsWith(DEPRECATED_PREFIX)) { + return; + } + logger.warn(String.format("Detected usage of deprecated prefix %s. This may be removed in a " + + "future version of Spring Cloud GCP. Please use the new prefix %s instead.", + DEPRECATED_PREFIX, PREFERRED_PREFIX)); + } + + @FunctionalInterface + public interface PrefixMatcher { + boolean matches(String input); + } +} diff --git a/spring-cloud-gcp-secretmanager/src/test/java/com/google/cloud/spring/secretmanager/SecretManagerPropertyUtilsTests.java b/spring-cloud-gcp-secretmanager/src/test/java/com/google/cloud/spring/secretmanager/SecretManagerPropertyUtilsTests.java index 436a20542b..77e235a401 100644 --- a/spring-cloud-gcp-secretmanager/src/test/java/com/google/cloud/spring/secretmanager/SecretManagerPropertyUtilsTests.java +++ b/spring-cloud-gcp-secretmanager/src/test/java/com/google/cloud/spring/secretmanager/SecretManagerPropertyUtilsTests.java @@ -19,14 +19,27 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy; import com.google.cloud.secretmanager.v1.SecretVersionName; import com.google.cloud.spring.core.GcpProjectIdProvider; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; class SecretManagerPropertyUtilsTests { private static final GcpProjectIdProvider DEFAULT_PROJECT_ID_PROVIDER = () -> "defaultProject"; + static Stream prefixes() { + return Stream.of( + Arguments.of("sm://"), + Arguments.of("sm@") + ); + } + @Test void testNonSecret() { String property = "spring.cloud.datasource"; @@ -36,9 +49,10 @@ void testNonSecret() { assertThat(secretIdentifier).isNull(); } - @Test - void testInvalidSecretFormat_missingSecretId() { - String property = "sm://"; + @ParameterizedTest + @MethodSource("prefixes") + void testInvalidSecretFormat_missingSecretId(String prefix) { + String property = prefix + ""; assertThatThrownBy( () -> @@ -48,9 +62,10 @@ void testInvalidSecretFormat_missingSecretId() { .hasMessageContaining("The GCP Secret Manager secret id must not be empty"); } - @Test - void testShortProperty_secretId() { - String property = "sm://the-secret"; + @ParameterizedTest + @MethodSource("prefixes") + void testShortProperty_secretId(String prefix) { + String property = prefix + "the-secret"; SecretVersionName secretIdentifier = SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); @@ -59,9 +74,10 @@ void testShortProperty_secretId() { assertThat(secretIdentifier.getSecretVersion()).isEqualTo("latest"); } - @Test - void testShortProperty_projectSecretId() { - String property = "sm://the-secret/the-version"; + @ParameterizedTest + @MethodSource("prefixes") + void testShortProperty_projectSecretId(String prefix) { + String property = prefix + "the-secret/the-version"; SecretVersionName secretIdentifier = SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); @@ -70,9 +86,10 @@ void testShortProperty_projectSecretId() { assertThat(secretIdentifier.getSecretVersion()).isEqualTo("the-version"); } - @Test - void testShortProperty_projectSecretIdVersion() { - String property = "sm://my-project/the-secret/2"; + @ParameterizedTest + @MethodSource("prefixes") + void testShortProperty_projectSecretIdVersion(String prefix) { + String property = prefix + "my-project/the-secret/2"; SecretVersionName secretIdentifier = SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); @@ -81,9 +98,10 @@ void testShortProperty_projectSecretIdVersion() { assertThat(secretIdentifier.getSecretVersion()).isEqualTo("2"); } - @Test - void testLongProperty_projectSecret() { - String property = "sm://projects/my-project/secrets/the-secret"; + @ParameterizedTest + @MethodSource("prefixes") + void testLongProperty_projectSecret(String prefix) { + String property = prefix + "projects/my-project/secrets/the-secret"; SecretVersionName secretIdentifier = SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); @@ -92,9 +110,10 @@ void testLongProperty_projectSecret() { assertThat(secretIdentifier.getSecretVersion()).isEqualTo("latest"); } - @Test - void testLongProperty_projectSecretVersion() { - String property = "sm://projects/my-project/secrets/the-secret/versions/3"; + @ParameterizedTest + @MethodSource("prefixes") + void testLongProperty_projectSecretVersion(String prefix) { + String property = prefix + "projects/my-project/secrets/the-secret/versions/3"; SecretVersionName secretIdentifier = SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); diff --git a/spring-cloud-gcp-secretmanager/src/test/java/com/google/cloud/spring/secretmanager/SecretManagerSyntaxUtilsTest.java b/spring-cloud-gcp-secretmanager/src/test/java/com/google/cloud/spring/secretmanager/SecretManagerSyntaxUtilsTest.java new file mode 100644 index 0000000000..7edcace04c --- /dev/null +++ b/spring-cloud-gcp-secretmanager/src/test/java/com/google/cloud/spring/secretmanager/SecretManagerSyntaxUtilsTest.java @@ -0,0 +1,333 @@ +package com.google.cloud.spring.secretmanager; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.Marker; + +public class SecretManagerSyntaxUtilsTest { + + @Test + public void testDeprecatedSyntaxWarns() { + FakeLogger logger = new FakeLogger(); + SecretManagerSyntaxUtils.warnIfUsingDeprecatedSyntax(logger, "sm@my-secret"); + assertThat(logger.getWarnCounter()).isEqualTo(0); + SecretManagerSyntaxUtils.warnIfUsingDeprecatedSyntax(logger, "sm://my-secret"); + assertThat(logger.getWarnCounter()).isEqualTo(1); + } + + public class FakeLogger implements Logger { + + private int warnCounter; + + public int getWarnCounter() { + return warnCounter; + } + + @Override + public void warn(String s) { + warnCounter++; + } + + @Override + public void warn(String s, Object o) { + warnCounter++; + } + + @Override + public void warn(String s, Object... objects) { + warnCounter++; + } + + @Override + public void warn(String s, Object o, Object o1) { + warnCounter++; + } + + @Override + public void warn(String s, Throwable throwable) { + warnCounter++; + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return true; + } + + @Override + public void warn(Marker marker, String s) { + warnCounter++; + } + + @Override + public void warn(Marker marker, String s, Object o) { + warnCounter++; + } + + @Override + public void warn(Marker marker, String s, Object o, Object o1) { + warnCounter++; + } + + @Override + public void warn(Marker marker, String s, Object... objects) { + warnCounter++; + } + + @Override + public void warn(Marker marker, String s, Throwable throwable) { + warnCounter++; + } + + @Override + public String getName() { + return ""; + } + + @Override + public boolean isTraceEnabled() { + return false; + } + + @Override + public void trace(String s) { + + } + + @Override + public void trace(String s, Object o) { + + } + + @Override + public void trace(String s, Object o, Object o1) { + + } + + @Override + public void trace(String s, Object... objects) { + + } + + @Override + public void trace(String s, Throwable throwable) { + + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return false; + } + + @Override + public void trace(Marker marker, String s) { + + } + + @Override + public void trace(Marker marker, String s, Object o) { + + } + + @Override + public void trace(Marker marker, String s, Object o, Object o1) { + + } + + @Override + public void trace(Marker marker, String s, Object... objects) { + + } + + @Override + public void trace(Marker marker, String s, Throwable throwable) { + + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void debug(String s) { + + } + + @Override + public void debug(String s, Object o) { + + } + + @Override + public void debug(String s, Object o, Object o1) { + + } + + @Override + public void debug(String s, Object... objects) { + + } + + @Override + public void debug(String s, Throwable throwable) { + + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return false; + } + + @Override + public void debug(Marker marker, String s) { + + } + + @Override + public void debug(Marker marker, String s, Object o) { + + } + + @Override + public void debug(Marker marker, String s, Object o, Object o1) { + + } + + @Override + public void debug(Marker marker, String s, Object... objects) { + + } + + @Override + public void debug(Marker marker, String s, Throwable throwable) { + + } + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public void info(String s) { + + } + + @Override + public void info(String s, Object o) { + + } + + @Override + public void info(String s, Object o, Object o1) { + + } + + @Override + public void info(String s, Object... objects) { + + } + + @Override + public void info(String s, Throwable throwable) { + + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return false; + } + + @Override + public void info(Marker marker, String s) { + + } + + @Override + public void info(Marker marker, String s, Object o) { + + } + + @Override + public void info(Marker marker, String s, Object o, Object o1) { + + } + + @Override + public void info(Marker marker, String s, Object... objects) { + + } + + @Override + public void info(Marker marker, String s, Throwable throwable) { + + } + + @Override + public boolean isWarnEnabled() { + return true; + } + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public void error(String s) { + + } + + @Override + public void error(String s, Object o) { + + } + + @Override + public void error(String s, Object o, Object o1) { + + } + + @Override + public void error(String s, Object... objects) { + + } + + @Override + public void error(String s, Throwable throwable) { + + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return false; + } + + @Override + public void error(Marker marker, String s) { + + } + + @Override + public void error(Marker marker, String s, Object o) { + + } + + @Override + public void error(Marker marker, String s, Object o, Object o1) { + + } + + @Override + public void error(Marker marker, String s, Object... objects) { + + } + + @Override + public void error(Marker marker, String s, Throwable throwable) { + + } + } +}