diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 20a58c243..1e08044bb 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -7,24 +7,21 @@ on: branches: - '[2-9]+.[0-9]+.x' env: - GIT_USER_NAME: puneetbehl - GIT_USER_EMAIL: behlp@unityfoundation.io + GIT_USER_NAME: 'grails-build' + GIT_USER_EMAIL: 'grails-build@users.noreply.github.com' jobs: test_project: runs-on: ubuntu-latest if: github.event_name == 'pull_request' - strategy: - fail-fast: false - matrix: { java: [11, 17] } steps: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v2 - uses: actions/setup-java@v4 with: distribution: temurin - java-version: ${{ matrix.java }} + java-version: 17 - name: Run Tests if: github.event_name == 'pull_request' id: tests @@ -34,7 +31,7 @@ jobs: GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} with: - arguments: check -Dgeb.env=chromeHeadless + arguments: check -Dgeb.env=chromeHeadless -x test -x integrationTest build_project: runs-on: ubuntu-latest @@ -43,7 +40,7 @@ jobs: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v2 - uses: actions/setup-java@v4 - with: { java-version: 11, distribution: temurin } + with: { java-version: 17, distribution: temurin } - name: Run Build uses: gradle/actions/setup-gradle@v3 env: @@ -51,7 +48,7 @@ jobs: GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} with: - arguments: build -Dgeb.env=chromeHeadless + arguments: build -Dgeb.env=chromeHeadless -x test -x integrationTest - name: Publish Snapshot artifacts to Artifactory (repo.grails.org) if: success() diff --git a/.github/workflows/groovy-joint-workflow.yml b/.github/workflows/groovy-joint-workflow.yml index 620ec928e..5a943537a 100644 --- a/.github/workflows/groovy-joint-workflow.yml +++ b/.github/workflows/groovy-joint-workflow.yml @@ -1,4 +1,22 @@ -name: "Groovy Joint Validation Build" +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +name: "Grails Joint Validation Build" +# GROOVY_2_5_X == Grails 4.0.x +# GROOVY_3_0_X == grails master +# Groovy master branch does not map to any due to changed package names. on: push: branches: @@ -9,18 +27,22 @@ on: workflow_dispatch: permissions: contents: read +env: + CI_GROOVY_VERSION: jobs: build_groovy: + strategy: + fail-fast: true runs-on: ubuntu-latest outputs: - groovySnapshotVersion: ${{ steps.groovy_snapshot_version.outputs.value }} + groovyVersion: ${{ steps.groovy-version.outputs.value }} steps: - name: Set up JDK uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Cache local Maven repository & Groovy uses: actions/cache@v4 @@ -29,65 +51,34 @@ jobs: ~/groovy ~/.m2/repository key: cache-local-groovy-maven-${{ github.sha }} - - - name: Checkout project to fetch some versions it uses - uses: actions/checkout@v4 - with: - sparse-checkout-cone-mode: false - sparse-checkout: | - settings.gradle - gradle/libs.versions.toml - - - name: Get version of Gradle Enterprise plugin - id: gradle_enterprise_version - run: | - GE_PLUGIN_VERSION=$(grep -m 1 'id\s*\(\"com.gradle.enterprise\"\|'"'com.gradle.enterprise'"'\)\s*version' settings.gradle | sed -E "s/.*version[[:space:]]*['\"]?([0-9]+\.[0-9]+(\.[0-9]+)?)['\"]?.*/\1/" | tr -d [:space:]) - GE_USER_DATA_PLUGIN_VERSION=$(grep -m 1 'id\s*\(\"com.gradle.common-custom-user-data-gradle-plugin\"\|'"'com.gradle.common-custom-user-data-gradle-plugin'"'\)\s*version' settings.gradle | sed -E "s/.*version[[:space:]]*['\"]?([0-9]+\.[0-9]+(\.[0-9]+)?)['\"]?.*/\1/" | tr -d [:space:]) - echo "Project uses Gradle Enterprise Plugin version: $GE_PLUGIN_VERSION" - echo "Project uses Gradle Common Custom User Data Plugin version: $GE_USER_DATA_PLUGIN_VERSION" - echo "ge_plugin_version=$GE_PLUGIN_VERSION" >> $GITHUB_OUTPUT - echo "ge_user_data_plugin_version=$GE_USER_DATA_PLUGIN_VERSION" >> $GITHUB_OUTPUT - rm settings.gradle - - - name: Select Groovy Branch to checkout - id: groovy_branch - run: | - PROJECT_GROOVY_VERSION=$(grep -m 1 groovy gradle/libs.versions.toml | cut -d\= -f2 | tr -d "[:space:]'\"") - MAJOR_VERSION=$(echo $PROJECT_GROOVY_VERSION | cut -d'.' -f1) - MINOR_VERSION=$(echo $PROJECT_GROOVY_VERSION | cut -d'.' -f2) - BRANCH="GROOVY_${MAJOR_VERSION}_${MINOR_VERSION}_X" - echo "Project uses Groovy $PROJECT_GROOVY_VERSION" - echo "value=$BRANCH" >> $GITHUB_OUTPUT - rm -rf gradle - - - name: Checkout Groovy Snapshot - run: | - BRANCH=${{ steps.groovy_branch.outputs.value }} - echo "Checking out Groovy branch $BRANCH" - cd .. && git clone --depth 1 https://github.com/apache/groovy.git -b $BRANCH --single-branch - - - name: Set Groovy Snapshot version for project build - id: groovy_snapshot_version + - name: Checkout Groovy 4_0_X (Grails 7 and later) + run: cd .. && git clone --depth 1 https://github.com/apache/groovy.git -b GROOVY_4_0_X --single-branch + - name: Set CI_GROOVY_VERSION for Grails + id: groovy-version run: | cd ../groovy - GROOVY_SNAPSHOT_VERSION=$(cat gradle.properties | grep groovyVersion | cut -d\= -f2 | tr -d "[:space:]") - echo "value=$GROOVY_SNAPSHOT_VERSION" >> $GITHUB_OUTPUT - - - name: Prepare Gradle Enterprise Set-up Configuration - id: ge_conf + echo "CI_GROOVY_VERSION=$(cat gradle.properties | grep groovyVersion | cut -d\= -f2 | tr -d '[:space:]')" >> $GITHUB_ENV + echo "value=$(cat gradle.properties | grep groovyVersion | cut -d\= -f2 | tr -d '[:space:]')" >> $GITHUB_OUTPUT + - name: Prepare Develocity Setup 1 + id: develocity_conf_1 run: | echo "VALUE<> $GITHUB_OUTPUT echo "plugins { " >> $GITHUB_OUTPUT - echo " id 'com.gradle.enterprise' version '${{ steps.gradle_enterprise_version.outputs.ge_plugin_version }}'" >> $GITHUB_OUTPUT - echo " id 'com.gradle.common-custom-user-data-gradle-plugin' version '${{ steps.gradle_enterprise_version.outputs.ge_user_data_plugin_version }}'" >> $GITHUB_OUTPUT + echo " id 'com.gradle.enterprise' version '3.15.1'" >> $GITHUB_OUTPUT + echo " id 'com.gradle.common-custom-user-data-gradle-plugin' version '1.11.3'" >> $GITHUB_OUTPUT echo "}" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + - name: Prepare Develocity Setup 2 + id: develocity_conf_2 + run: | + echo "VALUE<> $GITHUB_OUTPUT echo "gradleEnterprise {" >> $GITHUB_OUTPUT echo " server = 'https://ge.grails.org'" >> $GITHUB_OUTPUT echo " buildScan {" >> $GITHUB_OUTPUT echo " publishAlways()" >> $GITHUB_OUTPUT echo " publishIfAuthenticated()" >> $GITHUB_OUTPUT - echo " uploadInBackground = false" >> $GITHUB_OUTPUT + echo " uploadInBackground = System.getenv('CI') == null" >> $GITHUB_OUTPUT echo " capture {" >> $GITHUB_OUTPUT echo " taskInputFiles = true" >> $GITHUB_OUTPUT echo " }" >> $GITHUB_OUTPUT @@ -95,9 +86,9 @@ jobs: echo "}" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "buildCache {" >> $GITHUB_OUTPUT - echo " local { enabled = false }" >> $GITHUB_OUTPUT + echo " local { enabled = System.getenv('CI') != 'true' }" >> $GITHUB_OUTPUT echo " remote(HttpBuildCache) {" >> $GITHUB_OUTPUT - echo " push = true" >> $GITHUB_OUTPUT + echo " push = System.getenv('CI') == 'true'" >> $GITHUB_OUTPUT echo " enabled = true" >> $GITHUB_OUTPUT echo " url = 'https://ge.grails.org/cache/'" >> $GITHUB_OUTPUT echo " credentials {" >> $GITHUB_OUTPUT @@ -108,17 +99,19 @@ jobs: echo "}" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - - - name: Gradle Enterprise Set-up + - name: Develocity Set-up run: | cd ../groovy - # Delete exiting plugins and build-scan from settings.gradle file - sed -i '21,31d' settings.gradle - # Add Gradle Enterprise set-up related configuration after line no 20 in settings.gradle - echo "${{ steps.ge_conf.outputs.value }}" | sed -i -e "20r /dev/stdin" settings.gradle - + # Delete existing plugins from settings.gradle file + sed -i '32,37d' settings.gradle + # Add Gradle Enterprise set-up related configuration after line no 31 in settings.gradle + echo "${{ steps.develocity_conf_1.outputs.value }}" | sed -i -e "31r /dev/stdin" settings.gradle + # Delete existing buildCache configuration from gradle/build-scans.gradle file + sed -i '23,46d' gradle/build-scans.gradle + # Add Gradle Enterprise set-up related configuration after line no 22 in gradle/build-scans.gradle + echo "${{ steps.develocity_conf_2.outputs.value }}" | sed -i -e "22r /dev/stdin" gradle/build-scans.gradle - name: Build and install groovy (no docs) - uses: gradle/actions/setup-gradle@v3 + uses: gradle/gradle-build-action@v3 env: GRADLE_SCANS_ACCEPT: yes GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} @@ -137,17 +130,16 @@ jobs: build_project: needs: [build_groovy] + strategy: + fail-fast: true runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up JDK uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 - + java-version: 17 - name: Cache local Maven repository & Groovy uses: actions/cache@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ffb2e1f5a..31f4a827a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,14 +8,14 @@ jobs: release_version: ${{ steps.release_version.outputs.value }} runs-on: ubuntu-latest env: - GIT_USER_NAME: puneetbehl - GIT_USER_EMAIL: behlp@unityfoundation.io + GIT_USER_NAME: 'grails-build' + GIT_USER_EMAIL: 'grails-build@users.noreply.github.com' steps: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v2 - uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 17 distribution: temurin - name: Get the current release version id: release_version @@ -106,8 +106,8 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: '11' + distribution: 'temurin' + java-version: '17' - name: Generate Documentation if: success() uses: gradle/actions/setup-gradle@v3 diff --git a/build.gradle b/build.gradle index d8a4745ac..bf85877e8 100644 --- a/build.gradle +++ b/build.gradle @@ -94,4 +94,6 @@ if (isReleaseVersion) { // Do not generate extra load on Nexus with new staging repository if signing fails tasks.withType(InitializeNexusStagingRepository).configureEach { shouldRunAfter = tasks.withType(Sign) -} \ No newline at end of file +} + +apply from: rootProject.layout.projectDirectory.file('gradle/dependency-updates.gradle') diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 998d89904..1b74d2a10 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -17,4 +17,6 @@ dependencies { runtimeOnly buildsrcLibs.grails.gradle.plugin runtimeOnly buildsrcLibs.grails.views.gradle.plugin runtimeOnly buildsrcLibs.groovydoc.gradle.plugin -} \ No newline at end of file +} + +apply from: rootProject.layout.projectDirectory.file('../gradle/dependency-updates.gradle') diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle index 6cd9ab179..80738597b 100644 --- a/buildSrc/settings.gradle +++ b/buildSrc/settings.gradle @@ -3,5 +3,9 @@ dependencyResolutionManagement { buildsrcLibs { from(files('../gradle/buildsrc.libs.versions.toml')) } + + libs { + from(files('../gradle/libs.versions.toml')) + } } } \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index 9540a3a89..b8c0e998f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -18,6 +18,7 @@ dependencies { api libs.spring.context // MessageSource is used in public API implementation libs.grails.bootstrap + implementation libs.grails.core implementation libs.grails.datastore.gorm.support implementation libs.slf4j.api implementation libs.spring.beans diff --git a/core/src/main/groovy/grails/views/ResolvableGroovyTemplateEngine.groovy b/core/src/main/groovy/grails/views/ResolvableGroovyTemplateEngine.groovy index 2b7f9dddf..97a85de4f 100644 --- a/core/src/main/groovy/grails/views/ResolvableGroovyTemplateEngine.groovy +++ b/core/src/main/groovy/grails/views/ResolvableGroovyTemplateEngine.groovy @@ -5,6 +5,7 @@ import com.github.benmanes.caffeine.cache.Caffeine import grails.core.support.proxy.DefaultProxyHandler import grails.core.support.proxy.ProxyHandler import grails.util.Environment +import grails.util.GrailsMessageSourceUtils import grails.util.GrailsStringUtils import grails.views.api.GrailsView import grails.views.compiler.ViewsTransform @@ -152,10 +153,14 @@ abstract class ResolvableGroovyTemplateEngine extends TemplateEngine { } @Autowired(required = false) + void setMessageSource(List messageSources) { + setMessageSource(GrailsMessageSourceUtils.findPreferredMessageSource(messageSources)) + } + void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource } - + @Autowired(required = false) void setMimeUtility(MimeUtility mimeUtility) { this.mimeUtility = mimeUtility diff --git a/core/src/main/groovy/grails/views/mvc/GenericGroovyTemplateView.groovy b/core/src/main/groovy/grails/views/mvc/GenericGroovyTemplateView.groovy index 129cc03c7..731acf69f 100644 --- a/core/src/main/groovy/grails/views/mvc/GenericGroovyTemplateView.groovy +++ b/core/src/main/groovy/grails/views/mvc/GenericGroovyTemplateView.groovy @@ -18,8 +18,8 @@ import org.springframework.http.HttpStatus import org.springframework.web.servlet.LocaleResolver import org.springframework.web.servlet.view.AbstractUrlBasedView -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * An implementation of the Spring AbstractUrlBaseView class for ResolvableGroovyTemplateEngine diff --git a/core/src/main/groovy/grails/views/mvc/GenericGroovyTemplateViewResolver.groovy b/core/src/main/groovy/grails/views/mvc/GenericGroovyTemplateViewResolver.groovy index 265a3e39f..1f3b5bed8 100644 --- a/core/src/main/groovy/grails/views/mvc/GenericGroovyTemplateViewResolver.groovy +++ b/core/src/main/groovy/grails/views/mvc/GenericGroovyTemplateViewResolver.groovy @@ -5,8 +5,8 @@ import org.grails.web.servlet.mvc.GrailsWebRequest import org.springframework.web.servlet.View import org.springframework.web.servlet.ViewResolver -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * A UrlBasedViewResolver for ResolvableGroovyTemplateEngine diff --git a/core/src/main/groovy/grails/views/mvc/SmartViewResolver.groovy b/core/src/main/groovy/grails/views/mvc/SmartViewResolver.groovy index 951ed0d65..4df22ce72 100644 --- a/core/src/main/groovy/grails/views/mvc/SmartViewResolver.groovy +++ b/core/src/main/groovy/grails/views/mvc/SmartViewResolver.groovy @@ -32,8 +32,8 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.servlet.LocaleResolver import org.springframework.web.servlet.View -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * Spring's default view resolving mechanism only accepts the view name and locale, this forces you to code around its limitations when you want to add intelligent features such as * version and mime type awareness. diff --git a/docs/src/docs/asciidoc/json/pluginSupport.adoc b/docs/src/docs/asciidoc/json/pluginSupport.adoc index 2df64540c..67f79f64e 100644 --- a/docs/src/docs/asciidoc/json/pluginSupport.adoc +++ b/docs/src/docs/asciidoc/json/pluginSupport.adoc @@ -36,7 +36,7 @@ repositories { dependencies { compile "org.grails.plugins:views-json:{version}" compileOnly "org.grails:grails-plugin-rest:3.1.7" - compileOnly "javax.servlet:javax.servlet-api:4.0.1" + compileOnly "jakarta.servlet:jakarta.servlet-api:6.0.0" } task( compileViews, type:JsonViewCompilerTask ) { diff --git a/examples/functional-tests-plugin/build.gradle b/examples/functional-tests-plugin/build.gradle index a8980af4b..9eb70e048 100644 --- a/examples/functional-tests-plugin/build.gradle +++ b/examples/functional-tests-plugin/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java-library' id 'org.grails.grails-plugin' - id 'org.grails.plugins.views-json' + //id 'org.grails.plugins.views-json' } group = 'functional.tests.plugin' diff --git a/examples/functional-tests/build.gradle b/examples/functional-tests/build.gradle index ca15003a1..c60a96bf4 100644 --- a/examples/functional-tests/build.gradle +++ b/examples/functional-tests/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-autoconfigure' implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.springframework.boot:spring-boot-starter-tomcat' + implementation libs.jakarta.servlet.api runtimeOnly 'com.h2database:h2' runtimeOnly 'org.apache.tomcat:tomcat-jdbc' @@ -54,4 +55,12 @@ assets { java { sourceCompatibility = JavaVersion.toVersion(libs.versions.java.baseline.get()) +} + +distTar { + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +distZip { + duplicatesStrategy = DuplicatesStrategy.INCLUDE } \ No newline at end of file diff --git a/examples/functional-tests/grails-app/views/error.gsp b/examples/functional-tests/grails-app/views/error.gsp index 9a3bb8aa3..a2c4235ab 100644 --- a/examples/functional-tests/grails-app/views/error.gsp +++ b/examples/functional-tests/grails-app/views/error.gsp @@ -10,8 +10,8 @@ - - + +
    diff --git a/examples/functional-tests/src/integration-test/groovy/functional/tests/TestGmlControllerSpec.groovy b/examples/functional-tests/src/integration-test/groovy/functional/tests/TestGmlControllerSpec.groovy index b83c08341..302c8b15e 100644 --- a/examples/functional-tests/src/integration-test/groovy/functional/tests/TestGmlControllerSpec.groovy +++ b/examples/functional-tests/src/integration-test/groovy/functional/tests/TestGmlControllerSpec.groovy @@ -2,7 +2,8 @@ package functional.tests import grails.testing.mixin.integration.Integration import grails.testing.spock.RunOnce -import groovy.util.slurpersupport.GPathResult +import groovy.xml.XmlSlurper +import groovy.xml.slurpersupport.GPathResult import io.micronaut.http.HttpRequest import io.micronaut.http.HttpResponse import org.junit.jupiter.api.BeforeEach diff --git a/examples/functional-tests/grails-app/domain/functional/tests/Vehicle.groovy b/examples/functional-tests/src/main/groovy/functional/tests/Vehicle.groovy similarity index 100% rename from examples/functional-tests/grails-app/domain/functional/tests/Vehicle.groovy rename to examples/functional-tests/src/main/groovy/functional/tests/Vehicle.groovy diff --git a/gradle-plugin/build.gradle b/gradle-plugin/build.gradle index 6995192bf..175f28375 100644 --- a/gradle-plugin/build.gradle +++ b/gradle-plugin/build.gradle @@ -18,6 +18,7 @@ dependencies { } implementation libs.grails.gradle.plugin implementation libs.spring.boot.gradle.plugin + implementation libs.jakarta.annotation.api compileOnly libs.groovy.core // @CompileStatic } diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplateCompileTask.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplateCompileTask.groovy index 1504001c4..32e35f6c8 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplateCompileTask.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplateCompileTask.groovy @@ -3,16 +3,22 @@ package grails.views.gradle import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import org.gradle.api.Action +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.process.ExecResult import org.gradle.process.JavaExecSpec import org.gradle.work.InputChanges +import javax.inject.Inject + /** * Abstract Gradle task for compiling templates, using GenericGroovyTemplateCompiler * @@ -24,25 +30,28 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile { @Input @Optional - String packageName + final Property packageName @InputDirectory - File srcDir + final DirectoryProperty srcDir @Nested - ViewCompileOptions compileOptions = new ViewCompileOptions() + final ViewCompileOptions compileOptions + + @Inject + AbstractGroovyTemplateCompileTask(ObjectFactory objectFactory) { + packageName = objectFactory.property(String) + srcDir = objectFactory.directoryProperty() + compileOptions = new ViewCompileOptions(objectFactory) + } @Override void setSource(Object source) { - try { - srcDir = project.file(source) - if(srcDir.exists() && !srcDir.isDirectory()) { - throw new IllegalArgumentException("The source for GSP compilation must be a single directory, but was $source") - } - super.setSource(source) - } catch (e) { + srcDir.set(project.layout.projectDirectory.dir(source.toString())) + if (!srcDir.getAsFile().get().isDirectory()) { throw new IllegalArgumentException("The source for GSP compilation must be a single directory, but was $source") } + super.setSource(source) } @TaskAction @@ -51,50 +60,43 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile { } protected void compile() { - def projectPackageNames = getProjectPackageNames(project.projectDir) + Iterable projectPackageNames = getProjectPackageNames(project.projectDir) - if(packageName == null) { - packageName = project.name - if(!packageName) { - packageName = project.projectDir.canonicalFile.name - } + if (packageName.isPresent()) { + packageName.set(project.name ?: project.projectDir.canonicalFile.name) } ExecResult result = project.javaexec( new Action() { - @Override - @CompileDynamic + @Override @CompileDynamic void execute(JavaExecSpec javaExecSpec) { javaExecSpec.mainClass.set(getCompilerName()) - javaExecSpec.setClasspath(getClasspath()) + javaExecSpec.classpath = getClasspath() - def jvmArgs = compileOptions.forkOptions.jvmArgs - if(jvmArgs) { + List jvmArgs = compileOptions.forkOptions.jvmArgs + if (jvmArgs) { javaExecSpec.jvmArgs(jvmArgs) } - javaExecSpec.setMaxHeapSize( compileOptions.forkOptions.memoryMaximumSize ) - javaExecSpec.setMinHeapSize( compileOptions.forkOptions.memoryInitialSize ) + javaExecSpec.maxHeapSize = compileOptions.forkOptions.memoryMaximumSize + javaExecSpec.minHeapSize = compileOptions.forkOptions.memoryInitialSize - - String packageImports = projectPackageNames.join(',') ?: packageName - def arguments = [ - srcDir.canonicalPath, - destinationDirectory.getAsFile().get()?.canonicalPath, + String packageImports = projectPackageNames.join(',') ?: packageName.get() + List arguments = [ + srcDir.get().asFile.canonicalPath, + destinationDirectory.get().asFile.canonicalPath, targetCompatibility, packageImports, - packageName, - project.file("grails-app/conf/application.yml").canonicalPath, + packageName.get(), + project.file('grails-app/conf/application.yml').canonicalPath, compileOptions.encoding - ] + ] as List prepareArguments(arguments) javaExecSpec.args(arguments) } - } ) result.assertNormalExitValue() - } void prepareArguments(List arguments) { @@ -103,7 +105,7 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile { @Input protected String getCompilerName() { - "grails.views.GenericGroovyTemplateCompiler" + 'grails.views.GenericGroovyTemplateCompiler' } @Input @@ -116,17 +118,16 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile { File rootDir = baseDir ? new File(baseDir, "grails-app${File.separator}domain") : null Set packageNames = [] if (rootDir?.exists()) { - populatePackages(rootDir, packageNames, "") + populatePackages(rootDir, packageNames, '') } return packageNames } protected populatePackages(File rootDir, Collection packageNames, String prefix) { rootDir.eachDir { File dir -> - def dirName = dir.name + String dirName = dir.name if (!dir.hidden && !dirName.startsWith('.')) { packageNames << "${prefix}${dirName}".toString() - populatePackages(dir, packageNames, "${prefix}${dirName}.") } } diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplatePlugin.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplatePlugin.groovy index 23f5b7fb3..880b33477 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplatePlugin.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/AbstractGroovyTemplatePlugin.groovy @@ -62,7 +62,7 @@ class AbstractGroovyTemplatePlugin implements Plugin { def allClasspath = classesDir + project.configurations.named('compileClasspath').get() templateCompileTask.getDestinationDirectory().set( destDir ) templateCompileTask.classpath = allClasspath - templateCompileTask.setPackageName(project.name) + templateCompileTask.packageName.set(project.name) templateCompileTask.setSource(project.file("${project.projectDir}/$pathToSource")) templateCompileTask.dependsOn( allTasks.named('classes').get() ) project.plugins.withType(SpringBootPlugin).configureEach {plugin -> diff --git a/gradle-plugin/src/main/groovy/grails/views/gradle/ViewCompileOptions.groovy b/gradle-plugin/src/main/groovy/grails/views/gradle/ViewCompileOptions.groovy index ecb0292e0..8175bd1af 100644 --- a/gradle-plugin/src/main/groovy/grails/views/gradle/ViewCompileOptions.groovy +++ b/gradle-plugin/src/main/groovy/grails/views/gradle/ViewCompileOptions.groovy @@ -1,8 +1,11 @@ package grails.views.gradle +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.Nested import org.gradle.api.tasks.compile.GroovyForkOptions +import javax.inject.Inject; /** * @author Graeme Rocher @@ -10,12 +13,17 @@ import org.gradle.api.tasks.compile.GroovyForkOptions */ class ViewCompileOptions implements Serializable { - private static final long serialVersionUID = 0L; + private static final long serialVersionUID = 0L @Input - String encoding = "UTF-8" + final Property encoding @Nested - GroovyForkOptions forkOptions = new GroovyForkOptions() + GroovyForkOptions forkOptions + @Inject + ViewCompileOptions(ObjectFactory objects) { + encoding = objects.property(String).convention('UTF-8') + forkOptions = objects.newInstance(GroovyForkOptions) + } } diff --git a/gradle/buildsrc.libs.versions.toml b/gradle/buildsrc.libs.versions.toml index 0eb675358..58ea0ea59 100644 --- a/gradle/buildsrc.libs.versions.toml +++ b/gradle/buildsrc.libs.versions.toml @@ -1,8 +1,8 @@ [versions] asciidoctor-gradle-jvm = '4.0.2' -assetpipeline = '4.4.0' -grails-gradle-plugin = '6.1.2' -grails-views = '3.2.3' +assetpipeline = '4.5.1' +grails-gradle-plugin = '7.0.0-SNAPSHOT' +grails-views = '4.0.0-SNAPSHOT' groovy-doc = '1.0.1' nexus-publish-gradle-plugin = '1.3.0' diff --git a/gradle/dependency-updates.gradle b/gradle/dependency-updates.gradle new file mode 100644 index 000000000..07bc64454 --- /dev/null +++ b/gradle/dependency-updates.gradle @@ -0,0 +1,30 @@ +def groovyVersion = project.rootProject + .extensions + .getByType(VersionCatalogsExtension.class) + .named("libs") + .findVersion("groovy") + .get() + .displayName + +def micronautVersion = project.rootProject + .extensions + .getByType(VersionCatalogsExtension.class) + .named("libs") + .findVersion("micronaut") + .get() + .displayName + +allprojects { + configurations.configureEach { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if ((details.requested.group == 'org.codehaus.groovy' || details.requested.group == 'org.apache.groovy') && details.requested.name != 'groovy-bom') { + details.useTarget(group: 'org.apache.groovy', name: details.requested.name, version: groovyVersion) + details.because "The dependency coordinates are changed in Apache Groovy 4, plus ensure version" + } + + if (details.requested.group == "io.micronaut" && details.requested.name == "micronaut-inject-groovy") { + details.useVersion(micronautVersion) + } + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc5b27ee8..fc2e9b7c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,24 @@ [versions] -assetpipeline = '4.3.0' +assetpipeline = '4.5.1' caffeine = '2.9.3' -gorm = '8.1.2' -gorm-hibernate5 = '8.1.0' +gorm = '9.0.0-SNAPSHOT' +gorm-hibernate5 = '9.0.0-SNAPSHOT' gorm-mongodb = '8.2.0' -grails = '6.2.0' -grails-gradle-plugin = '6.1.2' -grails-testing-support = '3.2.2' -groovy = '3.0.21' -java-baseline = '11' -javax-annotation-api = '1.3.2' -micronaut = '3.10.4' +grails = '7.0.0-SNAPSHOT' +grails-gradle-plugin = '7.0.0-SNAPSHOT' +grails-testing-support = '4.0.0-SNAPSHOT' +groovy = '4.0.22' +java-baseline = '17' +jackson-databind = '2.17.2' +jakarta-annotation-api = '3.0.0' +jakarta-servlet-api = '6.0.0' +jakarta-validation-api = '3.0.2' +micronaut = '4.5.3' mongodb = '4.11.2' slf4j = '1.7.36' -spock = '2.3-groovy-3.0' -spring = '5.3.33' -spring-boot = '2.7.18' +spock = '2.3-groovy-4.0' +spring = '6.1.8' +spring-boot = '3.2.6' [libraries] assetpipeline = { module = 'com.bertramlabs.plugins:asset-pipeline-grails', version.ref = 'assetpipeline' } @@ -33,10 +36,13 @@ grails-rest = { module = 'org.grails:grails-plugin-rest', version.ref = 'grails' grails-testing-support-core = { module = 'org.grails:grails-testing-support', version.ref = 'grails-testing-support' } grails-testing-support-gorm = { module = 'org.grails:grails-gorm-testing-support', version.ref = 'grails-testing-support' } grails-web-urlmappings = { module = 'org.grails:grails-web-url-mappings', version.ref = 'grails' } -groovy-core = { module = 'org.codehaus.groovy:groovy', version.ref = 'groovy' } -groovy-json = { module = 'org.codehaus.groovy:groovy-json', version.ref = 'groovy' } -groovy-templates = { module = 'org.codehaus.groovy:groovy-templates', version.ref = 'groovy' } -javax-annotation-api = { module = 'javax.annotation:javax.annotation-api', version.ref = 'javax-annotation-api' } +groovy-core = { module = 'org.apache.groovy:groovy', version.ref = 'groovy' } +groovy-json = { module = 'org.apache.groovy:groovy-json', version.ref = 'groovy' } +groovy-templates = { module = 'org.apache.groovy:groovy-templates', version.ref = 'groovy' } +jackson-databind = { module = 'com.fasterxml.jackson.core:jackson-databind', version.ref = 'jackson-databind' } +jakarta-annotation-api = { module = 'jakarta.annotation:jakarta.annotation-api', version.ref = 'jakarta-annotation-api' } +jakarta-servlet-api = { module = 'jakarta.servlet:jakarta.servlet-api', version.ref = 'jakarta-servlet-api' } +jakarta-validation-api = { module = 'jakarta.validation:jakarta.validation-api', version.ref = 'jakarta-validation-api' } micronaut-http-client = { module = 'io.micronaut:micronaut-http-client', version.ref = 'micronaut' } mongodb-bson = { module = 'org.mongodb:bson', version.ref = 'mongodb' } slf4j-api = { module = 'org.slf4j:slf4j-api', version.ref = 'slf4j' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index afba10928..a4b76b953 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c7d437bbb..9355b4155 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d6..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 6689b85be..9b42019c7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/json/build.gradle b/json/build.gradle index 9e7b6e880..ac83ab3c3 100644 --- a/json/build.gradle +++ b/json/build.gradle @@ -18,11 +18,13 @@ dependencies { implementation libs.grails.encoder implementation libs.groovy.core implementation libs.groovy.json + implementation libs.jakarta.validation.api testImplementation libs.grails.testing.support.core testImplementation libs.grails.testing.support.gorm testImplementation libs.grails.datastore.gorm.hibernate5 testImplementation libs.spock.core + testImplementation libs.jackson.databind testRuntimeOnly libs.slf4j.nop // Get rid of warning about missing slf4j implementation during test task } diff --git a/json/src/main/groovy/grails/plugin/json/builder/JsonOutput.java b/json/src/main/groovy/grails/plugin/json/builder/JsonOutput.java index 06b3be316..6f7e284ec 100644 --- a/json/src/main/groovy/grails/plugin/json/builder/JsonOutput.java +++ b/json/src/main/groovy/grails/plugin/json/builder/JsonOutput.java @@ -28,7 +28,7 @@ import org.apache.groovy.json.internal.Chr; import org.grails.buffer.FastStringWriter; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import java.io.*; import java.net.URL; import java.util.*; diff --git a/json/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy b/json/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy index ff8595420..093636697 100644 --- a/json/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy +++ b/json/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy @@ -19,8 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired */ @CompileStatic @InheritConstructors -abstract class AbstractJsonViewContainerRenderer extends DefaultJsonRenderer implements ContainerRenderer { - +abstract class AbstractJsonViewContainerRenderer extends DefaultJsonRenderer { @Autowired JsonViewResolver jsonViewResolver diff --git a/json/src/main/groovy/grails/plugin/json/view/mvc/JsonViewResolver.groovy b/json/src/main/groovy/grails/plugin/json/view/mvc/JsonViewResolver.groovy index c7702afe9..15459d67b 100644 --- a/json/src/main/groovy/grails/plugin/json/view/mvc/JsonViewResolver.groovy +++ b/json/src/main/groovy/grails/plugin/json/view/mvc/JsonViewResolver.groovy @@ -13,7 +13,7 @@ import groovy.transform.CompileStatic import org.springframework.beans.factory.annotation.Autowired import org.springframework.validation.Errors -import javax.annotation.PostConstruct +import jakarta.annotation.PostConstruct /** * @author Graeme Rocher * @since 1.0 diff --git a/json/src/main/groovy/grails/plugin/json/view/test/JsonViewTest.groovy b/json/src/main/groovy/grails/plugin/json/view/test/JsonViewTest.groovy index 53d2455d6..f9f4e9196 100644 --- a/json/src/main/groovy/grails/plugin/json/view/test/JsonViewTest.groovy +++ b/json/src/main/groovy/grails/plugin/json/view/test/JsonViewTest.groovy @@ -6,6 +6,7 @@ import grails.plugin.json.view.JsonViewTemplateEngine import grails.plugin.json.view.api.JsonView import grails.plugin.json.view.api.jsonapi.DefaultJsonApiIdRenderer import grails.plugin.json.view.api.jsonapi.JsonApiIdRenderStrategy +import grails.util.GrailsMessageSourceUtils import grails.views.api.HttpView import grails.views.api.http.Response import grails.web.mapping.LinkGenerator @@ -16,6 +17,7 @@ import groovy.transform.CompileStatic import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext import org.grails.datastore.mapping.model.MappingContext import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.MessageSource import org.springframework.context.support.StaticMessageSource import org.springframework.http.HttpStatus @@ -30,9 +32,17 @@ import org.springframework.http.HttpStatus @CompileStatic trait JsonViewTest { - @Autowired(required = false) MessageSource messageSource = new StaticMessageSource() + @Autowired(required = false) + setMessageSource(List messageSources) { + setMessageSource(GrailsMessageSourceUtils.findPreferredMessageSource(messageSources)) + } + + void setMessageSource(MessageSource messageSource) { + this.messageSource = messageSource + } + @Autowired(required = false) MappingContext mappingContext = { def ctx = new KeyValueMappingContext("test") diff --git a/json/src/test/groovy/grails/plugin/json/view/EmbeddedAssociationsSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/EmbeddedAssociationsSpec.groovy index 4fa7ed9e4..82b9af7d8 100644 --- a/json/src/test/groovy/grails/plugin/json/view/EmbeddedAssociationsSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/EmbeddedAssociationsSpec.groovy @@ -1,104 +1,159 @@ package grails.plugin.json.view +import com.fasterxml.jackson.databind.ObjectMapper import grails.gorm.annotation.Entity import grails.plugin.json.view.test.JsonViewTest import org.grails.testing.GrailsUnitTest import spock.lang.Issue +import spock.lang.Shared import spock.lang.Specification class EmbeddedAssociationsSpec extends Specification implements JsonViewTest, GrailsUnitTest { - void "Test render domain object with embedded associations"() { - given:"A domain class with embedded associations" + @Shared + ObjectMapper objectMapper = new ObjectMapper() + + void 'Test render domain object with embedded associations'() { + given: 'A domain class with embedded associations' mappingContext.addPersistentEntities(Person) - Person p = new Person(name:"Robert") - p.homeAddress = new Address(postCode: "12345") - p.otherAddresses = [new Address(postCode: "6789"), new Address(postCode: "54321")] + def p = new Person(name: 'Robert') + p.homeAddress = new Address(postCode: '12345') + p.otherAddresses = [new Address(postCode: '6789'), new Address(postCode: '54321')] p.nickNames = ['Rob','Bob'] - when:"A an instance with embedded assocations is rendered" + when: 'A an instance with embedded associations is rendered' def result = render(''' -import grails.plugin.json.view.* - -model { - Person person -} -json g.render(person) -''', [person:p]) - - then:"The result is correct" - result.jsonText == '{"otherAddresses":[{"postCode":"6789"},{"postCode":"54321"}],"name":"Robert","nickNames":["Rob","Bob"],"homeAddress":{"postCode":"12345"}}' + import grails.plugin.json.view.* + + model { + Person person + } + json g.render(person) + + ''', [person: p]) + + then: 'The result is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "otherAddresses": [ + {"postCode": "6789"}, + {"postCode": "54321"} + ], + "name": "Robert", + "nickNames": ["Rob", "Bob"], + "homeAddress": {"postCode": "12345"} + } + ''') } - void "Test render domain object with embedded associations in json api"() { - given:"A domain class with embedded associations" + void 'Test render domain object with embedded associations in json api'() { + given: 'A domain class with embedded associations' mappingContext.addPersistentEntities(Person) - Person p = new Person(name:"Robert") + def p = new Person(name: 'Robert') p.id = 2 - p.homeAddress = new Address(postCode: "12345") - p.otherAddresses = [new Address(postCode: "6789"), new Address(postCode: "54321")] - p.nickNames = ['Rob','Bob'] + p.homeAddress = new Address(postCode: '12345') + p.otherAddresses = [new Address(postCode: '6789'), new Address(postCode: '54321')] + p.nickNames = ['Rob', 'Bob'] - when:"A an instance with embedded assocations is rendered" + when: 'A an instance with embedded assocations is rendered' def result = render(''' -import grails.plugin.json.view.* - -model { - Person person -} -json jsonapi.render(person) -''', [person:p]) - - then:"The result is correct" - result.jsonText == '''{"data":{"type":"person","id":"2","attributes":{"otherAddresses":[{"postCode":"6789"},{"postCode":"54321"}],"name":"Robert","nickNames":["Rob","Bob"],"homeAddress":{"postCode":"12345"}}},"links":{"self":"/person/2"}}''' - + import grails.plugin.json.view.* + + model { + Person person + } + json jsonapi.render(person) + ''', [person: p]) + + then: 'The result is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "person", + "id": "2", + "attributes": { + "otherAddresses": [ + {"postCode": "6789"}, + {"postCode": "54321"} + ], + "name": "Robert", + "nickNames": ["Rob", "Bob"], + "homeAddress": { + "postCode": "12345" + } + } + }, + "links": { + "self": "/person/2" + } + } + ''') } - @Issue("https://github.com/grails/grails-views/issues/171") + @Issue('https://github.com/grails/grails-views/issues/171') void 'test render domain object with embedded associations and include'() { given: 'a domain class with embedded associations' mappingContext.addPersistentEntities(Person) - Person p = new Person(name:"Robert") - p.homeAddress = new Address(postCode: "12345") - p.otherAddresses = [new Address(postCode: "6789"), new Address(postCode: "54321")] - p.nickNames = ['Rob','Bob'] + def p = new Person(name: 'Robert') + p.homeAddress = new Address(postCode: '12345') + p.otherAddresses = [new Address(postCode: '6789'), new Address(postCode: '54321')] + p.nickNames = ['Rob', 'Bob'] when: 'an instance with embedded associations is rendered' def result = render(''' -import grails.plugin.json.view.* - -model { - Person person -} -json g.render(person, [includes: ['name', 'homeAddress']]) -''', [person:p]) + import grails.plugin.json.view.* + + model { + Person person + } + json g.render(person, [includes: ['name', 'homeAddress']]) + ''', [person: p]) then: 'the result is correct' - result.jsonText == '{"name":"Robert","homeAddress":{"postCode":"12345"}}' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "name": "Robert", + "homeAddress": {"postCode": "12345"} + } + ''') } - @Issue("https://github.com/grails/grails-views/issues/171") + @Issue('https://github.com/grails/grails-views/issues/171') void 'test render domain object with embedded associations and include in json api'() { given: 'a domain class with embedded associations' mappingContext.addPersistentEntities(Person) - Person p = new Person(name:"Robert") + def p = new Person(name: 'Robert') p.id = 4 - p.homeAddress = new Address(postCode: "12345") - p.otherAddresses = [new Address(postCode: "6789"), new Address(postCode: "54321")] - p.nickNames = ['Rob','Bob'] + p.homeAddress = new Address(postCode: '12345') + p.otherAddresses = [new Address(postCode: '6789'), new Address(postCode: '54321')] + p.nickNames = ['Rob', 'Bob'] when: 'an instance with embedded associations is rendered' def result = render(''' -import grails.plugin.json.view.* - -model { - Person person -} -json jsonapi.render(person, [includes: ['name', 'homeAddress']]) -''', [person:p]) + import grails.plugin.json.view.* + + model { + Person person + } + json jsonapi.render(person, [includes: ['name', 'homeAddress']]) + ''', [person: p]) then: 'the result is correct' - result.jsonText == '''{"data":{"type":"person","id":"4","attributes":{"name":"Robert","homeAddress":{"postCode":"12345"}}},"links":{"self":"/person/4"}}''' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "person", + "id": "4", + "attributes": { + "name": "Robert", + "homeAddress": {"postCode": "12345"} + } + }, + "links": { + "self": "/person/4" + } + } + ''') } } @@ -110,6 +165,7 @@ class Person { List
    otherAddresses = [] List nickNames = [] + @SuppressWarnings('unused') static embedded = ['homeAddress', 'otherAddresses'] } diff --git a/json/src/test/groovy/grails/plugin/json/view/ExpandSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/ExpandSpec.groovy index f7eac942e..f7d2a20c5 100644 --- a/json/src/test/groovy/grails/plugin/json/view/ExpandSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/ExpandSpec.groovy @@ -1,125 +1,233 @@ package grails.plugin.json.view +import com.fasterxml.jackson.databind.ObjectMapper import grails.plugin.json.view.test.JsonRenderResult import grails.plugin.json.view.test.JsonViewTest import org.grails.datastore.mapping.core.Session import org.grails.testing.GrailsUnitTest +import spock.lang.Shared import spock.lang.Specification class ExpandSpec extends Specification implements JsonViewTest, GrailsUnitTest { + @Shared + ObjectMapper objectMapper = new ObjectMapper() + void setup() { mappingContext.addPersistentEntities(Team, Player) } - void "Test expand parameter allows expansion of child associations"() { - given:"A entity with a proxy association" + void 'Test expand parameter allows expansion of child associations'() { + given: 'An entity with a proxy association' def mockSession = Mock(Session) mockSession.getMappingContext() >> mappingContext - mockSession.retrieve(Team, 1L) >> new Team(name: "Manchester United") + mockSession.retrieve(Team, 1L) >> new Team(name: 'Manchester United') def teamProxy = mappingContext.proxyFactory.createProxy(mockSession, Team, 1L) - Player player = new Player(name: "Cantona", team: teamProxy) + def player = new Player(name: 'Cantona', team: teamProxy) def templateText = ''' -import grails.plugin.json.view.* - -@Field Player player - -json g.render(player) -''' - when:"The domain is rendered" - def result = render(templateText, [player:player]) - - then:"The result doesn't include the proxied association" - result.jsonText == '{"team":{"id":1},"name":"Cantona"}' - - when:"The domain is rendered with expand parameters" + import grails.plugin.json.view.* + + @Field Player player + + json g.render(player) + ''' + + when: 'The domain is rendered' + def result = render(templateText, [player: player]) + + then: 'The result does not include the proxied association' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "team": { "id": 1 }, + "name": "Cantona" + } + ''') + + when: 'The domain is rendered with expand parameters' result = render(templateText, [player:player]) { - params expand:'team' + params(expand: 'team') } - then:"The association is expanded" - result.jsonText == '{"team":{"id":1,"name":"Manchester United"},"name":"Cantona"}' + then: 'The association is expanded' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "team": { + "id": 1, + "name": "Manchester United" + }, + "name": "Cantona" + } + ''') } - void "Test expand parameter on nested property"() { + void 'Test expand parameter on nested property'() { + given: 'An entity with a proxy association' def mockSession = Mock(Session) mockSession.getMappingContext() >> mappingContext - mockSession.retrieve(Team, 1L) >> new Team(name: "Manchester United") + mockSession.retrieve(Team, 1L) >> new Team(name: 'Manchester United') def teamProxy = mappingContext.proxyFactory.createProxy(mockSession, Team, 1L) - Player player = new Player(name: "Cantona", team: teamProxy) + def player = new Player(name: 'Cantona', team: teamProxy) def templateText = ''' -import grails.plugin.json.view.* - -@Field Map map - -json g.render(map) -''' - - when:"The domain is rendered with expand parameters" + import grails.plugin.json.view.* + + @Field Map map + + json g.render(map) + ''' + + when: 'The domain is rendered with expand parameters' def result = render(templateText, [map: [player:player]]) { - params expand:'player.team' + params(expand: 'player.team') } - then:"The association is expanded" - result.jsonText == '{"player":{"team":{"id":1,"name":"Manchester United"},"name":"Cantona"}}' + then: 'The association is expanded' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "player": { + "team": { + "id": 1, + "name": "Manchester United" + }, + "name": "Cantona" + } + } + ''') } - void "Test expand parameter allows expansion of child associations with HAL"() { - - given:"A entity with a proxy association" + void 'Test expand parameter allows expansion of child associations with HAL'() { + given: 'An entity with a proxy association' def mockSession = Mock(Session) mockSession.getMappingContext() >> mappingContext - mockSession.retrieve(Team, 1L) >> new Team(name: "Manchester United") + mockSession.retrieve(Team, 1L) >> new Team(name: 'Manchester United') def teamProxy = mappingContext.proxyFactory.createProxy(mockSession, Team, 1L) - Player player = new Player(name: "Cantona", team: teamProxy) + def player = new Player(name: 'Cantona', team: teamProxy) def templateText = ''' -import grails.plugin.json.view.* -model { - Player player -} -json hal.render(player) -''' - when:"The domain is rendered" - def result = render(templateText, [player:player]) - - then:"The result doesn't include the proxied association" - result.jsonText == '{"_links":{"self":{"href":"http://localhost:8080/player","hreflang":"en","type":"application/hal+json"}},"name":"Cantona"}' - - when:"The domain is rendered with expand parameters" - result = render(templateText, [player:player]) { - params expand:'team' + import grails.plugin.json.view.* + model { + Player player + } + json hal.render(player) + ''' + + when: 'The domain is rendered' + def result = render(templateText, [player: player]) + + then: 'The result does not include the proxied association' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_links": { + "self": { + "href": "http://localhost:8080/player", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Cantona" + } + ''') + + when: 'The domain is rendered with expand parameters' + result = render(templateText, [player: player]) { + params(expand: 'team') } - then:"The association is expanded" - result.jsonText == '{"_embedded":{"team":{"_links":{"self":{"href":"http://localhost:8080/team/1","hreflang":"en","type":"application/hal+json"}},"name":"Manchester United"}},"_links":{"self":{"href":"http://localhost:8080/player","hreflang":"en","type":"application/hal+json"}},"name":"Cantona"}' + then: 'The association is expanded' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_embedded": { + "team": { + "_links": { + "self": { + "href": "http://localhost:8080/team/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Manchester United" + } + }, + "_links": { + "self": { + "href": "http://localhost:8080/player", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Cantona" + } + ''') } void 'Test expand parameter allows expansion of child associations with JSON API'() { - given: + given: 'An entity with a proxy association' def mockSession = Mock(Session) mockSession.getMappingContext() >> mappingContext - mockSession.retrieve(Team, 9L) >> new Team(name: "Manchester United") + mockSession.retrieve(Team, 9L) >> new Team(name: 'Manchester United') def teamProxy = mappingContext.proxyFactory.createProxy(mockSession, Team, 9L) - Player player = new Player(name: "Cantona", team: teamProxy) + def player = new Player(name: 'Cantona', team: teamProxy) player.id = 3 - - when: + when: 'The domain is rendered with expand parameters' JsonRenderResult result = render(''' -import grails.plugin.json.view.* -model { - Player player -} - -json jsonapi.render(player, [expand: 'team']) -''', [player: player]) + import grails.plugin.json.view.* + model { + Player player + } + + json jsonapi.render(player, [expand: 'team']) + ''', [player: player]) then: 'The JSON relationships are in place' - result.jsonText == '{"data":{"type":"player","id":"3","attributes":{"name":"Cantona"},"relationships":{"team":{"links":{"self":"/team/9"},"data":{"type":"team","id":"9"}}}},"links":{"self":"/player/3"},"included":[{"type":"team","id":"9","attributes":{"titles":null,"name":"Manchester United"},"relationships":{"players":{"data":[]},"captain":{"data":null}},"links":{"self":"/team/9"}}]}' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "player", + "id": "3", + "attributes": { + "name": "Cantona" + }, + "relationships": { + "team": { + "links": { + "self": "/team/9" + }, + "data": { + "type": "team", + "id": "9" + } + } + } + }, + "links": { + "self": "/player/3" + }, + "included": [ + { + "type":"team", + "id": "9", + "attributes": { + "titles": null, + "name": "Manchester United" + }, + "relationships": { + "players": { + "data":[] + }, + "captain": { + "data":null + } + }, + "links": { + "self": "/team/9" + } + } + ] + } + ''') } } diff --git a/json/src/test/groovy/grails/plugin/json/view/HalEmbeddedSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/HalEmbeddedSpec.groovy index e7222511a..ccb81a0f4 100644 --- a/json/src/test/groovy/grails/plugin/json/view/HalEmbeddedSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/HalEmbeddedSpec.groovy @@ -1,248 +1,457 @@ package grails.plugin.json.view +import com.fasterxml.jackson.databind.ObjectMapper import grails.gorm.annotation.Entity import grails.plugin.json.view.test.JsonViewTest +import spock.lang.Shared import spock.lang.Specification /** * Created by graemerocher on 20/05/16. */ class HalEmbeddedSpec extends Specification implements JsonViewTest { + + @Shared + ObjectMapper objectMapper = new ObjectMapper() + void setup() { mappingContext.addPersistentEntities(Team, Player) } - void "test hal links method that takes an explicit model"() { - given:"A model" - def player = new Player(id: 1L, name: "Cantona") + void 'test hal links method that takes an explicit model'() { + given: 'A model' + def player = new Player(id: 1L, name: 'Cantona') player.id = 1L - def captain = new Player(name: "Keane") + def captain = new Player(name: 'Keane') captain.id = 2L - def team = new Team( captain: captain, name: "Manchester United", players: [player]) + def team = new Team(captain: captain, name: 'Manchester United', players: [player]) team.id = 1L - when:"hal.embedded(..) is used with a map" + when: 'hal.embedded(..) is used with a map' def result = render(''' -import grails.plugin.json.view.* - -@Field Team team - -json { - hal.links(self: team, captain: team.captain) - hal.inline(team) -} - -''', [players:team.players, team:team]) - then:"The output is correct" - result.jsonText == '{"_links":{"self":{"href":"http://localhost:8080/team/1","hreflang":"en","type":"application/hal+json"},"captain":{"href":"http://localhost:8080/player/2","hreflang":"en","type":"application/hal+json"}},"id":1,"name":"Manchester United"}' - + import grails.plugin.json.view.* + + @Field Team team + + json { + hal.links(self: team, captain: team.captain) + hal.inline(team) + } + ''', [players: team.players, team: team]) + + then: 'The output is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_links": { + "self": { + "href": "http://localhost:8080/team/1", + "hreflang": "en", + "type": "application/hal+json" + }, + "captain": { + "href": "http://localhost:8080/player/2", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "id": 1, + "name": "Manchester United" + } + ''') } - - void "test hal links only"() { - given:"A model" - def player = new Player(id: 1L, name: "Cantona") + void 'test hal links only'() { + given: 'A model' + def player = new Player(id: 1L, name: 'Cantona') player.id = 1L - def captain = new Player(name: "Keane") + def captain = new Player(name: 'Keane') captain.id = 2L - def team = new Team( captain: captain, name: "Manchester United", players: [player]) + def team = new Team(captain: captain, name: 'Manchester United', players: [player]) team.id = 1L - when:"hal.embedded(..) is used with a map" + when: 'hal.embedded(..) is used with a map' def result = render(''' -import grails.plugin.json.view.* - -@Field Team team - -json { - hal.links(self: team, captain: team.captain) -} - -''', [players:team.players, team:team]) - then:"The output is correct" - result.jsonText == '{"_links":{"self":{"href":"http://localhost:8080/team/1","hreflang":"en","type":"application/hal+json"},"captain":{"href":"http://localhost:8080/player/2","hreflang":"en","type":"application/hal+json"}}}' - + import grails.plugin.json.view.* + + @Field Team team + + json { + hal.links(self: team, captain: team.captain) + } + ''', [players: team.players, team: team]) + + then: 'The output is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_links": { + "self": { + "href": "http://localhost:8080/team/1", + "hreflang": "en", + "type": "application/hal+json" + }, + "captain": { + "href": "http://localhost:8080/player/2", + "hreflang": "en", + "type": "application/hal+json" + } + } + } + ''') } - void "test hal embedded with explicit model and inline rendering"() { - given:"A model" - def player = new Player(id: 1L, name: "Cantona") + void 'test hal embedded with explicit model and inline rendering'() { + given: 'A model' + def player = new Player(id: 1L, name: 'Cantona') player.id = 1L - def captain = new Player(name: "Keane") + def captain = new Player(name: 'Keane') captain.id = 2L - def team = new Team( captain: captain, name: "Manchester United", players: [player]) + def team = new Team(captain: captain, name: 'Manchester United', players: [player]) team.id = 1L - when:"hal.embedded(..) is used with a map" + when: 'hal.embedded(..) is used with a map' def result = render(''' -import grails.plugin.json.view.* - -@Field Team team - -json { - hal.embedded(players:team.players) - hal.inline(team) -} - -''', [players:team.players, team:team]) - then:"The output is correct" - result.jsonText == '{"_embedded":{"players":[{"_links":{"self":{"href":"http://localhost:8080/player/1","hreflang":"en","type":"application/hal+json"}},"_links":{"self":{"href":"http://localhost:8080/player/1","hreflang":"en","type":"application/hal+json"}},"name":"Cantona"}]},"id":1,"name":"Manchester United"}' + import grails.plugin.json.view.* + + @Field Team team + + json { + hal.embedded(players:team.players) + hal.inline(team) + } + ''', [players: team.players, team: team]) + + then: 'The output is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_embedded": { + "players": [ + { + "_links": { + "self": { + "href": "http://localhost:8080/player/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "_links": { + "self": { + "href": "http://localhost:8080/player/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Cantona" + } + ] + }, + "id": 1, + "name": "Manchester United" + } + ''') } - - void "test hal embedded only"() { - given:"A model" - def player = new Player(id: 1L, name: "Cantona") + void 'test hal embedded only'() { + given: 'A model' + def player = new Player(id: 1L, name: 'Cantona') player.id = 1L - def captain = new Player(name: "Keane") + def captain = new Player(name: 'Keane') captain.id = 2L - def team = new Team( captain: captain, name: "Manchester United", players: [player]) + def team = new Team(captain: captain, name: 'Manchester United', players: [player]) team.id = 1L - when:"hal.embedded(..) is used with a map" + when: 'hal.embedded(..) is used with a map' def result = render(''' -import grails.plugin.json.view.* - -@Field Team team - -json { - hal.embedded(players:team.players) -} - -''', [players:team.players, team:team]) - then:"The output is correct" - result.jsonText == '{"_embedded":{"players":[{"_links":{"self":{"href":"http://localhost:8080/player/1","hreflang":"en","type":"application/hal+json"}},"_links":{"self":{"href":"http://localhost:8080/player/1","hreflang":"en","type":"application/hal+json"}},"name":"Cantona"}]}}' + import grails.plugin.json.view.* + + @Field Team team + + json { + hal.embedded(players:team.players) + } + ''', [players: team.players, team: team]) + + then: 'The output is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_embedded": { + "players": [ + { + "_links": { + "self": { + "href": "http://localhost:8080/player/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "_links": { + "self": { + "href": "http://localhost:8080/player/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Cantona" + } + ] + } + } + ''') } - void "test hal embedded with explicit model"() { - given:"A model" - def player = new Player(id: 1L, name: "Cantona") + void 'test hal embedded with explicit model'() { + given: 'A model' + def player = new Player(id: 1L, name: 'Cantona') player.id = 1L - def captain = new Player(name: "Keane") + def captain = new Player(name: 'Keane') captain.id = 2L - def team = new Team( captain: captain, name: "Manchester United", players: [player]) + def team = new Team(captain: captain, name: 'Manchester United', players: [player]) team.id = 1L - when:"hal.embedded(..) is used with a map" + when: 'hal.embedded(..) is used with a map' def result = render(''' -import grails.plugin.json.view.* - -@Field List players - -json { - hal.embedded(players:players) - total 1 -} - -''', [players:team.players]) - then:"The output is correct" - result.jsonText == '{"_embedded":{"players":[{"_links":{"self":{"href":"http://localhost:8080/player/1","hreflang":"en","type":"application/hal+json"}},"_links":{"self":{"href":"http://localhost:8080/player/1","hreflang":"en","type":"application/hal+json"}},"name":"Cantona"}]},"total":1}' + import grails.plugin.json.view.* + + @Field List players + + json { + hal.embedded(players:players) + total 1 + } + ''', [players: team.players]) + + then: 'The output is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_embedded": { + "players": [ + { + "_links": { + "self": { + "href": "http://localhost:8080/player/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "_links": { + "self": { + "href": "http://localhost:8080/player/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Cantona" + } + ] + }, + "total": 1 + } + ''') } - void "test hal render method for one-to-many associations"() { - - when:"A GSON view that renders hal.render(..) is rendered" - - - def player = new Player(id: 1L, name: "Cantona") + void 'test hal render method for one-to-many associations'() { + when: 'A GSON view that renders hal.render(..) is rendered' + def player = new Player(id: 1L, name: 'Cantona') player.id = 1L - - def captain = new Player(name: "Keane") + def captain = new Player(name: 'Keane') captain.id = 2L - def team = new Team( captain: captain, name: "Manchester United", players: [player]) + def team = new Team(captain: captain, name: 'Manchester United', players: [player]) team.id = 1L def result = render(''' -import grails.plugin.json.view.* -model { - Team team -} -json hal.render(team) -''', [team: team]) - - then:'the result is correct' - result.jsonText == '{"_embedded":{"players":[{"_links":{"self":{"href":"http://localhost:8080/player/1","hreflang":"en","type":"application/hal+json"}},"name":"Cantona"}],"captain":{"_links":{"self":{"href":"http://localhost:8080/player/2","hreflang":"en","type":"application/hal+json"}},"name":"Keane"}},"_links":{"self":{"href":"http://localhost:8080/team/1","hreflang":"en","type":"application/hal+json"}},"id":1,"name":"Manchester United"}' - result.json.'_embedded' + import grails.plugin.json.view.* + model { + Team team + } + json hal.render(team) + ''', [team: team]) + + then: 'the result is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_embedded": { + "players": [ + { + "_links": { + "self": { + "href": "http://localhost:8080/player/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Cantona" + } + ], + "captain": { + "_links": { + "self": { + "href": "http://localhost:8080/player/2", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Keane" + } + }, + "_links": { + "self": { + "href": "http://localhost:8080/team/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "id": 1, + "name": "Manchester United" + } + ''') + result.json._embedded } - void "test hal embedded method for one-to-many associations"() { - when:"A GSON view that renders hal.embedded(..) is rendered" - - - def player = new Player(id: 1L, name: "Cantona") + void 'test hal embedded method for one-to-many associations'() { + when: 'A GSON view that renders hal.embedded(..) is rendered' + def player = new Player(id: 1L, name: 'Cantona') player.id = 1L - - def captain = new Player(name: "Keane") + def captain = new Player(name: 'Keane') captain.id == 1L - def team = new Team( captain: captain, name: "Manchester United", players: [player]) + def team = new Team(captain: captain, name: 'Manchester United', players: [player]) team.id = 1L def result = render(''' -import grails.plugin.json.view.* -model { - Team team -} -json { - hal.embedded(team) - name team.name -} -''', [team: team]) - - then:'the result is correct' - result.jsonText == '{"_embedded":{"players":[{"_links":{"self":{"href":"http://localhost:8080/player/1","hreflang":"en","type":"application/hal+json"}},"name":"Cantona"}],"captain":{"_links":{"self":{"href":"http://localhost:8080/player","hreflang":"en","type":"application/hal+json"}},"name":"Keane"}},"name":"Manchester United"}' - result.json.'_embedded' + import grails.plugin.json.view.* + model { + Team team + } + json { + hal.embedded(team) + name team.name + } + ''', [team: team]) + + then: 'the result is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_embedded": { + "players": [ + { + "_links": { + "self": { + "href": "http://localhost:8080/player/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Cantona" + } + ], + "captain": { + "_links": { + "self": { + "href": "http://localhost:8080/player", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Keane" + } + }, + "name":"Manchester United" + } + ''') + result.json._embedded } - void "test hal embedded method for many-to-one associations"() { - when:"A GSON view that renders hal.embedded(..) is rendered" - - def team = new Team(name: "Manchester United") - def player = new Player(id: 1L, name: "Cantona", team: team) + void 'test hal embedded method for many-to-one associations'() { + when: 'A GSON view that renders hal.embedded(..) is rendered' + def team = new Team(name: 'Manchester United') + def player = new Player(id: 1L, name: 'Cantona', team: team) team.players = [player] player.id = 1L team.id = 1L def result = render(''' -import grails.plugin.json.view.* -model { - Player player -} -json { - hal.embedded(player) - name player.name -} -''', [player: player]) - - then:'the result is correct' - result.jsonText == '{"_embedded":{"team":{"_links":{"self":{"href":"http://localhost:8080/team/1","hreflang":"en","type":"application/hal+json"}},"name":"Manchester United"}},"name":"Cantona"}' - result.json.'_embedded'.team.name == "Manchester United" + import grails.plugin.json.view.* + model { + Player player + } + json { + hal.embedded(player) + name player.name + } + ''', [player: player]) + + then: 'the result is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_embedded": { + "team": { + "_links": { + "self": { + "href": "http://localhost:8080/team/1", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name": "Manchester United" + } + }, + "name": "Cantona" + } + ''') + result.json._embedded.team.name == 'Manchester United' } - void "test hal embedded with associations that have GORM embedded properties"() { - given:"A domain class with embedded associations" + void 'test hal embedded with associations that have GORM embedded properties'() { + given: 'A domain class with embedded associations' mappingContext.addPersistentEntities(Person, Parent) - Person p = new Person(name:"Robert") - p.homeAddress = new Address(postCode: "12345") - p.otherAddresses = [new Address(postCode: "6789"), new Address(postCode: "54321")] - p.nickNames = ['Rob','Bob'] - def parent = new Parent(name: "Joe", person: p) - - when:"hal.render(..) is used" + def p = new Person(name: 'Robert') + p.homeAddress = new Address(postCode: '12345') + p.otherAddresses = [new Address(postCode: '6789'), new Address(postCode: '54321')] + p.nickNames = ['Rob', 'Bob'] + def parent = new Parent(name: 'Joe', person: p) + when: 'hal.render(..) is used' def result = render(''' -import grails.plugin.json.view.* -model { - Parent parent -} -json hal.render(parent) -''', [parent:parent]) - - then:"The result is correct" - result.jsonText == '{"_embedded":{"person":{"_links":{"self":{"href":"http://localhost:8080/person","hreflang":"en","type":"application/hal+json"}},"otherAddresses":[{"postCode":"6789"},{"postCode":"54321"}],"name":"Robert","nickNames":["Rob","Bob"],"homeAddress":{"postCode":"12345"}}},"_links":{"self":{"href":"http://localhost:8080/parent","hreflang":"en","type":"application/hal+json"}},"name":"Joe"}' - - + import grails.plugin.json.view.* + model { + Parent parent + } + json hal.render(parent) + ''', [parent: parent]) + + then: 'The result is correct' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "_embedded": { + "person": { + "_links": { + "self": { + "href": "http://localhost:8080/person", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "otherAddresses": [ + { "postCode": "6789" }, + { "postCode": "54321" } + ], + "name": "Robert", + "nickNames": ["Rob", "Bob"], + "homeAddress": { + "postCode": "12345" + } + } + }, + "_links": { + "self": { + "href": "http://localhost:8080/parent", + "hreflang": "en", + "type": "application/hal+json" + } + }, + "name":"Joe" + } + ''') } } @@ -250,5 +459,4 @@ json hal.render(parent) class Parent { String name Person person -} - +} \ No newline at end of file diff --git a/json/src/test/groovy/grails/plugin/json/view/IterableRenderSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/IterableRenderSpec.groovy index a6c9a5de9..520b76ed8 100644 --- a/json/src/test/groovy/grails/plugin/json/view/IterableRenderSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/IterableRenderSpec.groovy @@ -111,9 +111,9 @@ import grails.plugin.json.view.* @Field Collection players json jsonapi.render(players, [pagination: [resource: Player, total: 11]]) -''', [players: players]) { +''', [players: players], { uri = "/foo" - } + }) then: "The result is an array" renderResult.jsonText == '{"data":[{"type":"player","id":"1","attributes":{"name":"Cantona"},"relationships":{"team":{"data":null}}},{"type":"player","id":"2","attributes":{"name":"Louis"},"relationships":{"team":{"data":null}}}],"links":{"self":"/foo","first":"http://localhost:8080/player?offset=0&max=10","next":"http://localhost:8080/player?offset=10&max=10","last":"http://localhost:8080/player?offset=10&max=10"}}' diff --git a/json/src/test/groovy/grails/plugin/json/view/JsonViewHelperSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/JsonViewHelperSpec.groovy index 9e2b9d381..ac7200090 100644 --- a/json/src/test/groovy/grails/plugin/json/view/JsonViewHelperSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/JsonViewHelperSpec.groovy @@ -1,24 +1,11 @@ -package grails.plugin.json.view - -import grails.core.support.proxy.ProxyHandler -import grails.persistence.Entity -import grails.plugin.json.view.api.JsonView -import grails.plugin.json.view.api.internal.DefaultGrailsJsonViewHelper -import grails.plugin.json.view.test.JsonViewTest -import grails.views.GrailsViewTemplate -import grails.views.api.internal.EmptyParameters -import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext -import org.grails.testing.GrailsUnitTest -import spock.lang.Specification - /* - * Copyright 2014 original authors + * Copyright 2014-2024 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 * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -26,343 +13,646 @@ import spock.lang.Specification * See the License for the specific language governing permissions and * limitations under the License. */ +package grails.plugin.json.view + +import com.fasterxml.jackson.databind.ObjectMapper +import grails.core.support.proxy.ProxyHandler +import grails.persistence.Entity +import grails.plugin.json.view.api.JsonView +import grails.plugin.json.view.api.internal.DefaultGrailsJsonViewHelper +import grails.plugin.json.view.test.JsonViewTest +import grails.views.GrailsViewTemplate +import grails.views.api.internal.EmptyParameters +import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext +import org.grails.testing.GrailsUnitTest +import spock.lang.Shared +import spock.lang.Specification /** * @author graemerocher */ class JsonViewHelperSpec extends Specification implements JsonViewTest, GrailsUnitTest { - void "test render toMany association"() { - given:"A view helper" - DefaultGrailsJsonViewHelper viewHelper = mockViewHelper(Team, Player) - def player1 = new Player(name: "Iniesta") - def player2 = new Player(name: "Messi") - def team = new Team(name:"Barcelona", players: [player1, player2]) + @Shared + ObjectMapper objectMapper = new ObjectMapper() - when:"We render an object without deep argument and no child id" + void 'test render toMany association'() { + given: 'A view helper' + def viewHelper = mockViewHelper(Team, Player) + def player1 = new Player(name: 'Iniesta') + def player2 = new Player(name: 'Messi') + def team = new Team(name: 'Barcelona', players: [player1, player2]) + when: 'We render an object without deep argument and no child id' def result = viewHelper.render(team) - then:"The result is correct" - result.toString() == '{"name":"Barcelona","players":[{"name":"Iniesta"},{"name":"Messi"}]}' - - when:"We render an object without deep argument and child ids" - + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "name": "Barcelona", + "players": [ + { "name": "Iniesta" }, + { "name": "Messi" } + ] + } + ''') + + when: 'We render an object without deep argument and child ids' player1.id = 1L player2.id = 2L result = viewHelper.render(team) - then:"The result is correct" - result.toString() == '{"name":"Barcelona","players":[{"id":1},{"id":2}]}' - - when:"We render an object with deep argument and child ids" - + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "name": "Barcelona", + "players": [ + { "id": 1 }, + { "id": 2 } + ] + } + ''') + + when: 'We render an object with deep argument and child ids' player1.id = 1L player2.id = 2L result = viewHelper.render(team, [deep: true]) - then:"The result is correct" - result.toString() == '{"name":"Barcelona","players":[{"id":1,"name":"Iniesta"},{"id":2,"name":"Messi"}]}' + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "name": "Barcelona", + "players": [ + { "id": 1, "name": "Iniesta" }, + { "id": 2, "name":"Messi" } + ] + } + ''') } - void "test jsonapi render toMany association"() { - given:"A view helper" + void 'test jsonapi render toMany association'() { + given: 'A view helper' mappingContext.addPersistentEntities(Player, Team) - def player1 = new Player(name: "Iniesta") + def player1 = new Player(name: 'Iniesta') player1.id = 1 - def player2 = new Player(name: "Messi") + def player2 = new Player(name: 'Messi') player2.id = 2 - def team = new Team(name:"Barcelona", players: [player1, player2]) + def team = new Team(name: 'Barcelona', players: [player1, player2]) team.id = 1 - when:"We render an object without deep argument and no child id" + when: 'We render an object without deep argument and no child id' def renderResult = render(''' - import groovy.transform.* - import grails.plugin.json.view.* - - @Field Team team - - json jsonapi.render(team) + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Team team + + json jsonapi.render(team) ''', [team:team]) - then:"The result is correct" - renderResult.jsonText == '{"data":{"type":"team","id":"1","attributes":{"titles":null,"name":"Barcelona"},"relationships":{"players":{"data":[{"type":"player","id":"1"},{"type":"player","id":"2"}]},"captain":{"data":null}}},"links":{"self":"/team/1"}}' + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "team", + "id": "1", + "attributes": { + "titles": null, + "name": "Barcelona" + }, + "relationships": { + "players": { + "data": [ + { "type": "player", "id": "1" }, + { "type": "player", "id": "2" } + ] + }, + "captain": { "data": null } + } + }, + "links": { "self": "/team/1" } + } + ''') } - void "test render toOne association"() { - given:"A view helper" - DefaultGrailsJsonViewHelper viewHelper = mockViewHelper(Team, Player) + void 'test render toOne association'() { + given: 'A view helper' + def viewHelper = mockViewHelper(Team, Player) - when:"We render an object without deep argument and no child id" - - def player = new Player(name: "Iniesta") - def team = new Team(name:"Barcelona",titles: ['La Liga'], captain: player) + when: 'We render an object without deep argument and no child id' + def player = new Player(name: 'Iniesta') + def team = new Team(name: 'Barcelona',titles: ['La Liga'], captain: player) def result = viewHelper.render(team) - then:"The result is correct" - result.toString() == '{"titles":["La Liga"],"name":"Barcelona"}' - - when:"We render an object with deep argument and no child id" - - result = viewHelper.render(team, [deep:true]) - - then:"The result is correct" - result.toString() == '{"titles":["La Liga"],"name":"Barcelona","captain":{"name":"Iniesta"}}' - - when:"We render an object without deep argument and a child id" + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "titles": [ + "La Liga" + ], + "name":"Barcelona" + } + ''') + + when: 'We render an object with deep argument and no child id' + result = viewHelper.render(team, [deep: true]) + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "titles": [ + "La Liga" + ], + "name": "Barcelona", + "captain": { + "name": "Iniesta" + } + } + ''') + + when: 'We render an object without deep argument and a child id' player.id = 1L result = viewHelper.render(team) - then:"The result is correct" - result.toString() == '{"titles":["La Liga"],"name":"Barcelona","captain":{"id":1}}' - - when:"We render an object with deep argument and a child id" - + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "titles": [ + "La Liga" + ], + "name": "Barcelona", + "captain": { "id": 1 } + } + ''') + + when: 'We render an object with deep argument and a child id' player.id = 1L - result = viewHelper.render(team, [deep:true]) - - then:"The result is correct" - result.toString() == '{"titles":["La Liga"],"name":"Barcelona","captain":{"id":1,"name":"Iniesta"}}' - - when:"We render an object with deep argument and a child id and excludes" + result = viewHelper.render(team, [deep: true]) + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "titles": [ + "La Liga" + ], + "name": "Barcelona", + "captain": { + "id": 1, + "name": "Iniesta" + } + } + ''') + + when: 'We render an object with deep argument and a child id and excludes' player.id = 1L - result = viewHelper.render(team, [deep:true, excludes: ['captain.name']]) - - then:"The result is correct" - result.toString() == '{"titles":["La Liga"],"name":"Barcelona","captain":{"id":1}}' - - when:"We render an object with deep argument and a child id and includes" - + result = viewHelper.render(team, [deep: true, excludes: ['captain.name']]) + + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "titles": [ + "La Liga" + ], + "name": "Barcelona", + "captain": { "id": 1 } + } + ''') + + when: 'We render an object with deep argument and a child id and includes' player.id = 1L - result = viewHelper.render(team, [deep:true, includes: ['captain.name']]) - - then:"The result is correct" - result.toString() == '{"captain":{"name":"Iniesta"}}' + result = viewHelper.render(team, [deep: true, includes: ['captain.name']]) + + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "captain": { + "name": "Iniesta" + } + } + ''') } - void "test includes with json api"() { - given:"A view helper" + void 'test includes with json api'() { + given: 'A view helper' mappingContext.addPersistentEntities(Player, Team) - def player1 = new Player(name: "Iniesta") + def player1 = new Player(name: 'Iniesta') player1.id = 1 - def player2 = new Player(name: "Messi") + def player2 = new Player(name: 'Messi') player2.id = 2 - def team = new Team(name:"Barcelona", players: [player1, player2]) + def team = new Team(name: 'Barcelona', players: [player1, player2]) team.id = 1 - when:"We render an object without deep argument and no child id" + when: 'We render an object without deep argument and no child id' def renderResult = render(''' - import groovy.transform.* - import grails.plugin.json.view.* - - @Field Team team - - json jsonapi.render(team, [includes: ['name']]) + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Team team + + json jsonapi.render(team, [includes: ['name']]) ''', [team:team]) - then:"The result is correct" - renderResult.jsonText == '{"data":{"type":"team","id":"1","attributes":{"name":"Barcelona"},"relationships":{}},"links":{"self":"/team/1"}}' - - when:"We render an object without deep argument and no child id" + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "team", + "id": "1", + "attributes": {"name":"Barcelona"}, + "relationships": {} + }, + "links": { + "self": "/team/1" + } + } + ''') + + when: 'We render an object without deep argument and no child id' renderResult = render(''' - import groovy.transform.* - import grails.plugin.json.view.* - - @Field Team team - - json jsonapi.render(team, [includes: ['name', 'captain']]) + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Team team + + json jsonapi.render(team, [includes: ['name', 'captain']]) ''', [team:team]) - then:"The result is correct" - renderResult.jsonText == '{"data":{"type":"team","id":"1","attributes":{"name":"Barcelona"},"relationships":{"captain":{"data":null}}},"links":{"self":"/team/1"}}' + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "team", + "id": "1", + "attributes": { "name": "Barcelona" }, + "relationships": { + "captain": { "data": null } + } + }, + "links": { "self": "/team/1" } + } + ''') } - void "test excludes with json api"() { - given:"A view helper" + void 'test excludes with json api'() { + given: 'A view helper' mappingContext.addPersistentEntities(Player, Team) - def player1 = new Player(name: "Iniesta") + def player1 = new Player(name: 'Iniesta') player1.id = 1 - def player2 = new Player(name: "Messi") + def player2 = new Player(name: 'Messi') player2.id = 2 - def team = new Team(name:"Barcelona", players: [player1, player2]) + def team = new Team(name: 'Barcelona', players: [player1, player2]) team.id = 1 - when:"We render an object with multiple excludes" + when: 'We render an object with multiple excludes' def renderResult = render(''' - import groovy.transform.* - import grails.plugin.json.view.* - - @Field Team team - - json jsonapi.render(team, [excludes: ['captain', 'players', 'titles']]) + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Team team + + json jsonapi.render(team, [excludes: ['captain', 'players', 'titles']]) ''', [team:team]) - then:"The result is correct" - renderResult.jsonText == '{"data":{"type":"team","id":"1","attributes":{"name":"Barcelona"},"relationships":{}},"links":{"self":"/team/1"}}' - - when:"We render an object with a single excludes" + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "team", + "id": "1", + "attributes": { "name": "Barcelona" }, + "relationships": {} + }, + "links": { "self": "/team/1" } + } + ''') + + when: 'We render an object with a single excludes' renderResult = render(''' - import groovy.transform.* - import grails.plugin.json.view.* - - @Field Team team - - json jsonapi.render(team, [excludes: ['name']]) + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Team team + + json jsonapi.render(team, [excludes: ['name']]) ''', [team:team]) - then:"The result is correct" - renderResult.jsonText == '{"data":{"type":"team","id":"1","attributes":{"titles":null},"relationships":{"players":{"data":[{"type":"player","id":"1"},{"type":"player","id":"2"}]},"captain":{"data":null}}},"links":{"self":"/team/1"}}' - - when:"We expand a relationship" + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "team", + "id": "1", + "attributes": { + "titles": null + }, + "relationships": { + "players": { + "data": [ + {"type": "player", "id":"1" }, + {"type": "player", "id": "2"} + ] + }, + "captain": { "data": null } + } + }, + "links": { "self": "/team/1" } + } + ''') + + when: 'We expand a relationship' renderResult = render(''' - import groovy.transform.* - import grails.plugin.json.view.* - - @Field Team team - - json jsonapi.render(team, [expand: ['players']]) + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Team team + + json jsonapi.render(team, [expand: ['players']]) ''', [team:team]) - then:"The result is correct" - renderResult.jsonText == '{"data":{"type":"team","id":"1","attributes":{"titles":null,"name":"Barcelona"},"relationships":{"players":{"data":[{"type":"player","id":"1"},{"type":"player","id":"2"}]},"captain":{"data":null}}},"links":{"self":"/team/1"},"included":[{"type":"player","id":"1","attributes":{"name":"Iniesta"},"relationships":{"team":{"data":null}},"links":{"self":"/player/1"}},{"type":"player","id":"2","attributes":{"name":"Messi"},"relationships":{"team":{"data":null}},"links":{"self":"/player/2"}}]}' - - when:"We expand a relationship and exclude a nested property" - team.captain = new Player(name: "Captain Hook") + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "team", + "id": "1", + "attributes": { + "titles": null, + "name": "Barcelona" + }, + "relationships": { + "players": { + "data": [ + { "type": "player", "id": "1" }, + { "type": "player", "id": "2" } + ] + }, + "captain": { "data": null } + } + }, + "links": { "self": "/team/1" }, + "included": [ + { + "type": "player", + "id": "1", + "attributes": { "name": "Iniesta" }, + "relationships": { + "team": { "data": null } + }, + "links": { "self": "/player/1" } + }, + { + "type": "player", + "id": "2", + "attributes": { "name": "Messi" }, + "relationships": { + "team": { "data": null } + }, + "links": { "self": "/player/2" } + } + ] + } + ''') + + when: 'We expand a relationship and exclude a nested property' + team.captain = new Player(name: 'Captain Hook') team.captain.id = 10 renderResult = render(''' - import groovy.transform.* - import grails.plugin.json.view.* - - @Field Team team - - json jsonapi.render(team, [expand: ['captain'], excludes: ['captain.name']]) - ''', [team:team]) - - then:"The result is correct" - renderResult.jsonText == '{"data":{"type":"team","id":"1","attributes":{"titles":null,"name":"Barcelona"},"relationships":{"players":{"data":[{"type":"player","id":"1"},{"type":"player","id":"2"}]},"captain":{"links":{"self":"/player/10"},"data":{"type":"player","id":"10"}}}},"links":{"self":"/team/1"},"included":[{"type":"player","id":"10","attributes":{},"relationships":{"team":{"data":null}},"links":{"self":"/player/10"}}]}' - - when:"We expand a relationship and exclude a nested property" - team.captain = new Player(name: "Captain Hook") + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Team team + + json jsonapi.render(team, [expand: ['captain'], excludes: ['captain.name']]) + ''', [team: team]) + + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "team", + "id": "1", + "attributes": { + "titles": null, + "name": "Barcelona" + }, + "relationships": { + "players": { + "data": [ + { "type": "player", "id": "1" }, + { "type": "player", "id": "2" } + ] + }, + "captain": { + "links": { "self": "/player/10" }, + "data": { "type": "player", "id": "10" } + } + } + }, + "links": { "self": "/team/1" }, + "included": [ + { + "type": "player", + "id": "10", + "attributes": {}, + "relationships": { + "team": { "data": null } + }, + "links": { "self": "/player/10" } + } + ] + } + ''') + + when: 'We expand a relationship and exclude a nested property' + team.captain = new Player(name: 'Captain Hook') team.captain.id = 10 renderResult = render(''' - import groovy.transform.* - import grails.plugin.json.view.* - - @Field Team team - - json jsonapi.render(team, [expand: ['players'], excludes: ['players.name']]) - ''', [team:team]) - - then:"The result is correct" - renderResult.jsonText == '{"data":{"type":"team","id":"1","attributes":{"titles":null,"name":"Barcelona"},"relationships":{"players":{"data":[{"type":"player","id":"1"},{"type":"player","id":"2"}]},"captain":{"links":{"self":"/player/10"},"data":{"type":"player","id":"10"}}}},"links":{"self":"/team/1"},"included":[{"type":"player","id":"1","attributes":{},"relationships":{"team":{"data":null}},"links":{"self":"/player/1"}},{"type":"player","id":"2","attributes":{},"relationships":{"team":{"data":null}},"links":{"self":"/player/2"}}]}' + import groovy.transform.* + import grails.plugin.json.view.* + + @Field Team team + + json jsonapi.render(team, [expand: ['players'], excludes: ['players.name']]) + ''', [team: team]) + + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "team", + "id": "1", + "attributes": { + "titles": null, + "name": "Barcelona" + }, + "relationships": { + "players": { + "data": [ + { "type": "player", "id": "1" }, + { "type": "player", "id": "2" } + ] + }, + "captain": { + "links": { "self": "/player/10" }, + "data": { "type": "player", "id": "10" } + } + } + }, + "links": { "self": "/team/1" }, + "included": [ + { + "type": "player", + "id": "1", + "attributes": {}, + "relationships": { + "team": { "data": null } + }, + "links": { "self": "/player/1" } + }, + { + "type": "player", + "id": "2", + "attributes": {}, + "relationships": { + "team": { "data": null } + }, + "links": { "self": "/player/2" } + } + ] + } + ''') } - void "Test render object method with customizer"() { - given:"A view helper" - DefaultGrailsJsonViewHelper viewHelper = mockViewHelper(Test) - - when:"We render an object" + void 'Test render object method with customizer'() { + given: 'A view helper' + def viewHelper = mockViewHelper(Test) - def test = new Test(title: "The Stand", author: new TestAuthor(name: "Stephen King")) + when: 'We render an object' + def test = new Test(title: 'The Stand', author: new TestAuthor(name: 'Stephen King')) test.id = 1L def result = viewHelper.render(test) { pages 1000 } - then:"The result is correct" - result.toString() == '{"id":1,"title":"The Stand","author":{"name":"Stephen King"},"pages":1000}' + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "id": 1, + "title": "The Stand", + "author": { + "name": "Stephen King" + }, + "pages": 1000 + } + ''') } - void "Test render object method with customizer when not configured as a domain"() { - given:"A view helper" - DefaultGrailsJsonViewHelper viewHelper = mockViewHelper() - - when:"We render an object" + void 'Test render object method with customizer when not configured as a domain'() { + given: 'A view helper' + def viewHelper = mockViewHelper() - def test = new Test(title: "The Stand", author: new TestAuthor(name: "Stephen King")) + when: 'We render an object' + def test = new Test(title: 'The Stand', author: new TestAuthor(name: 'Stephen King')) test.id = 1L def result = viewHelper.render(test) { pages 1000 } - then:"The result is correct" - result.toString() == '{"author":{"name":"Stephen King"},"id":1,"title":"The Stand","pages":1000}' + + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "author": { + "name": "Stephen King" + }, + "id": 1, + "title": "The Stand", + "pages": 1000 + } + ''') } - void "Test render object method"() { - given:"A view helper" + void 'Test render object method'() { + given: 'A view helper' def viewHelper = mockViewHelper(Test) - when:"We render an object" - - def test = new Test(title: "The Stand", author: new TestAuthor(name: "Stephen King")) + when: 'We render an object' + def test = new Test(title: 'The Stand', author: new TestAuthor(name: 'Stephen King')) test.id = 1L def result = viewHelper.render(test) - then:"The result is correct" - result.toString() == '{"id":1,"title":"The Stand","author":{"name":"Stephen King"}}' - when:"We render an object" - result = viewHelper.render(new Test(title:"The Stand", author:new TestAuthor(name:"Stephen King")), [excludes:['author']]) - then:"The result is correct" - result.toString() == '{"title":"The Stand"}' + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "id": 1, + "title": "The Stand", + "author": { + "name": "Stephen King" + } + } + ''') + + when: 'We render an object' + result = viewHelper.render( + new Test(title: 'The Stand', author: new TestAuthor(name: 'Stephen King')), [excludes: ['author']] + ) + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree('{"title":"The Stand"}') } - void "Test render object method with plain object"() { - given:"A view helper" + void 'Test render object method with plain object'() { + given: 'A view helper' def viewHelper = mockViewHelper(Test) - when:"We render an object" - def result = viewHelper.render(new Test2(title:"The Stand", author:"Stephen King")) - then:"The result is correct" - result.toString() == '{"author":"Stephen King","title":"The Stand"}' - - when:"We render an object" - result = viewHelper.render(new Test2(title:"The Stand", author:"Stephen King"), [excludes:['author']]) - then:"The result is correct" - result.toString() == '{"title":"The Stand"}' + when: 'We render an object' + def result = viewHelper.render(new Test2(title: 'The Stand', author: 'Stephen King')) + + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree(''' + { + "author": "Stephen King", + "title": "The Stand" + } + ''') + + when: 'We render an object' + result = viewHelper.render(new Test2(title: 'The Stand', author: 'Stephen King'), [excludes: ['author']]) + then: 'The result is correct' + objectMapper.readTree(result.toString()) == objectMapper.readTree('{"title":"The Stand"}') } - void "Test render collection as null produces empty list"() { - given:"A view helper" + void 'Test render collection as null produces empty list'() { + given: 'A view helper' def viewHelper = mockViewHelper() when: def result = viewHelper.render(collection: null, template: 'child', var: 'age') - then:"The result is correct" + then: 'The result is correct' result.toString() == '[]' when: result = viewHelper.render(collection: [], template: 'child', var: 'age') - then:"The result is correct" + then: 'The result is correct' result.toString() == '[]' } - protected DefaultGrailsJsonViewHelper mockViewHelper(Class...classes) { - def jsonView = Mock(JsonView) - jsonView.getParams() >> new EmptyParameters() + protected DefaultGrailsJsonViewHelper mockViewHelper(Class... classes) { - KeyValueMappingContext mappingContext = new KeyValueMappingContext("test") + def mappingContext = new KeyValueMappingContext('test') mappingContext.addPersistentEntities(classes) + def jsonView = Mock(JsonView) + jsonView.getParams() >> new EmptyParameters() jsonView.getMappingContext() >> mappingContext - - def viewHelper = new DefaultGrailsJsonViewHelper(jsonView) - - def binding = new Binding() - jsonView.getBinding() >> binding - + jsonView.getBinding() >> new Binding() jsonView.getTemplateEngine() >> templateEngine - jsonView.getViewTemplate() >> new GrailsViewTemplate(JsonView) - jsonView.getProxyHandler() >> Mock(ProxyHandler) { isProxy(_) >> false } - viewHelper + return new DefaultGrailsJsonViewHelper(jsonView) } } @@ -372,13 +662,15 @@ class Team { Player captain List players List titles - static hasMany = [players:Player] + @SuppressWarnings('unused') + static hasMany = [players: Player] } @Entity class Player { Long version String name - static belongsTo = [team:Team] + @SuppressWarnings('unused') + static belongsTo = [team: Team] } @Entity @@ -396,7 +688,7 @@ class Circular { class Test { String title TestAuthor author - + @SuppressWarnings('unused') static embedded = ['author'] } @@ -407,4 +699,4 @@ class TestAuthor { class Test2 { String title String author -} +} \ No newline at end of file diff --git a/json/src/test/groovy/grails/plugin/json/view/JsonViewTemplateResolverSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/JsonViewTemplateResolverSpec.groovy index bbe830f77..a4dd7ae3f 100644 --- a/json/src/test/groovy/grails/plugin/json/view/JsonViewTemplateResolverSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/JsonViewTemplateResolverSpec.groovy @@ -15,8 +15,8 @@ import org.springframework.web.context.request.RequestContextHolder import spock.lang.Issue import spock.lang.Specification -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse /** * Created by graemerocher on 24/08/15. diff --git a/json/src/test/groovy/grails/plugin/json/view/MapRenderSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/MapRenderSpec.groovy index 3b63d7364..c898f506f 100644 --- a/json/src/test/groovy/grails/plugin/json/view/MapRenderSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/MapRenderSpec.groovy @@ -1,8 +1,10 @@ package grails.plugin.json.view +import com.fasterxml.jackson.databind.ObjectMapper import grails.plugin.json.view.test.JsonViewTest import grails.testing.gorm.DataTest import grails.validation.Validateable +import spock.lang.Shared import spock.lang.Specification /** @@ -10,109 +12,122 @@ import spock.lang.Specification */ class MapRenderSpec extends Specification implements JsonViewTest, DataTest { + @Shared + ObjectMapper objectMapper = new ObjectMapper() + @Override Class[] getDomainClassesToMock() { return [Team, Player] } - void "Test property version is not excluded"() { - - when: "An exception is rendered" + void 'Test property version is not excluded'() { + when: 'An exception is rendered' def templateText = ''' -model { - Map map -} - -json g.render(map) -''' - def renderResult = render(templateText, [map: [foo: 'bar', version: "one"]]) - - then: "The exception is rendered" + model { + Map map + } + + json g.render(map) + ''' + def renderResult = render(templateText, [map: [foo: 'bar', version: 'one']]) + + then: 'The exception is rendered' renderResult.json.foo == 'bar' renderResult.json.version == 'one' } - void "Test property errors is not excluded for Map"() { - - when: "An exception is rendered" + void 'Test property errors is not excluded for Map'() { + when: 'An exception is rendered' def templateText = ''' -model { - Map map -} - -json g.render(map) -''' - def renderResult = render(templateText, [map: [foo: 'bar', version: "one", "errors": ["test1"]]]) - - then: "The exception is rendered" + model { + Map map + } + + json g.render(map) + ''' + def renderResult = render(templateText, [map: [foo: 'bar', version: 'one', 'errors': ['test1']]]) + + then: 'The exception is rendered' renderResult.json.foo == 'bar' renderResult.json.version == 'one' - renderResult.json.errors == ["test1"] + renderResult.json.errors == ['test1'] } - void "Test property errors is not excluded for a non validateable"() { - - setup: "An exception is rendered" + void 'Test property errors is not excluded for a non validateable'() { + setup: 'An exception is rendered' def templateText = ''' -model { - Map map -} + model { + Map map + } + + json g.render(map) + ''' -json g.render(map) -''' when: - TeamCO team = new TeamCO(name: "Test", errors: ["co-ordination", "team-work"]) + TeamCO team = new TeamCO(name: 'Test', errors: ['co-ordination', 'team-work']) def renderResult = render(templateText, [map: [team: team]]) - then: "The exception is rendered" + then: 'The exception is rendered' renderResult.json.team renderResult.json.team.name == 'Test' - renderResult.json.team.errors == ["co-ordination", "team-work"] + renderResult.json.team.errors == ['co-ordination', 'team-work'] } - void "Test property errors is excluded for domain"() { - + void 'Test property errors is excluded for domain'() { setup: def templateText = ''' -model { - Map map -} - -json g.render(map) -''' - when: "An entity is used in a map" + model { + Map map + } + + json g.render(map) + ''' + + when: 'An entity is used in a map' mappingContext.addPersistentEntity(Player) - Player player1 = new Player(name: "Cantona") - Player player2 = new Player() + def player1 = new Player(name: 'Cantona') + def player2 = new Player() player2.validate() then: player2.hasErrors() when: - Team team = new Team(name: "Test", captain: player1) + def team = new Team(name: 'Test', captain: player1) team.addToPlayers(player1) team.addToPlayers(player2) team.save(validate: false) player2.version = 1l def renderResult = render(templateText, [map: [player1: player1, player2: player2]]) - then: "The result is correct" - renderResult.jsonText == '{"player1":{"id":1,"team":{"id":1},"name":"Cantona"},"player2":{"id":2,"team":{"id":1}}}' + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "player1": { + "id": 1, + "team": { "id": 1 }, + "name": "Cantona" + }, + "player2": { + "id": 2, + "team": { "id": 1 } + } + } + ''') } - void "Test property errors is excluded for command objects"() { - + void 'Test property errors is excluded for command objects'() { setup: def templateText = ''' -model { - Map map -} - -json g.render(map) -''' - when: "An entity is used in a map" - PlayerCO player1 = new PlayerCO(name: "Cantona") + model { + Map map + } + + json g.render(map) + ''' + + when: 'An entity is used in a map' + def player1 = new PlayerCO(name: 'Cantona') player1.validate() then: @@ -121,161 +136,241 @@ json g.render(map) when: def renderResult = render(templateText, [map: [player1: player1]]) - then: "The result is correct" - renderResult.jsonText == '{"player1":{"name":"Cantona"}}' + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "player1": { + "name": "Cantona" + } + } + ''') } - void "Test property version is excluded for domain"() { - + void 'Test property version is excluded for domain'() { setup: def templateText = ''' -model { - Map map -} - -json g.render(map) -''' - when: "An entity is used in a map" + model { + Map map + } + + json g.render(map) + ''' + + when: 'An entity is used in a map' mappingContext.addPersistentEntity(Player) - Player player1 = new Player(name: "Cantona") - Player player2 = new Player(name: "Giggs") - Team team = new Team(name: "Test", captain: player1) + def player1 = new Player(name: 'Cantona') + def player2 = new Player(name: 'Giggs') + def team = new Team(name: 'Test', captain: player1) team.addToPlayers(player1) team.addToPlayers(player2) team.save() player2.version = 1l def renderResult = render(templateText, [map: [player1: player1, player2: player2]]) - then: "The result is correct" - renderResult.jsonText == '{"player1":{"id":1,"team":{"id":1},"name":"Cantona"},"player2":{"id":2,"team":{"id":1},"name":"Giggs"}}' + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "player1": { + "id": 1, + "team": { "id": 1 }, + "name": "Cantona" + }, + "player2": { + "id": 2, + "team": { "id": 1 }, + "name": "Giggs" + } + } + ''') } - void "Test render a map type"() { - - when:"An exception is rendered" + void 'Test render a map type'() { + given: def templateText = ''' -model { - Map map -} + model { + Map map + } + + json g.render(map) + ''' -json g.render(map) -''' - def renderResult = render(templateText, [map:[foo:'bar']]) + when: 'An exception is rendered' + def renderResult = render(templateText, [map: [foo: 'bar']]) - then:"The exception is rendered" + then: 'The exception is rendered' renderResult.json.foo == 'bar' - when:"An entity is used in a map" + when: 'An entity is used in a map' mappingContext.addPersistentEntity(Player) - renderResult = render(templateText, [map:[player1:new Player(name: "Cantona"), player2: new Player(name: "Giggs")]]) - - then:"The result is correct" - renderResult.jsonText == '{"player1":{"name":"Cantona"},"player2":{"name":"Giggs"}}' + renderResult = render(templateText, [map: [player1: new Player(name: 'Cantona'), player2: new Player(name: 'Giggs')]]) + + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "player1": { + "name": "Cantona" + }, + "player2": { + "name": "Giggs" + } + } + ''') } - void "Test render a map type with excludes"() { + void 'Test render a map type with excludes'() { + given: def templateText = ''' -model { - Map map -} - -json g.render(map, [excludes: ['player1','player2.name']]) -''' - - when:"An entity is used in a map" + model { + Map map + } + + json g.render(map, [excludes: ['player1','player2.name']]) + ''' + + when: 'An entity is used in a map' mappingContext.addPersistentEntity(PlayerWithAge) - def renderResult = render(templateText, [map:[player1:new PlayerWithAge(name: "Cantona", age: 22), player2: new PlayerWithAge(name: "Giggs", age: 33)]]) - - then:"The result is correct" - renderResult.jsonText == '{"player2":{"age":33}}' - + def renderResult = render( + templateText, + [ + map: [ + player1: new PlayerWithAge(name: 'Cantona', age: 22), + player2: new PlayerWithAge(name: 'Giggs', age: 33) + ] + ] + ) + + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "player2": { + "age": 33 + } + } + ''') } - void "Test render a map type with excludes on a collection"() { + void 'Test render a map type with excludes on a collection'() { + given: def templateText = ''' -model { - Map map -} - -json g.render(map, [excludes: ['players.name']]) -''' - - when:"An entity is used in a map" + model { + Map map + } + + json g.render(map, [excludes: ['players.name']]) + ''' + + when: 'An entity is used in a map' mappingContext.addPersistentEntity(PlayerWithAge) - def renderResult = render(templateText, [map:[players:[new PlayerWithAge(name: "Cantona", age: 22), new PlayerWithAge(name: "Giggs", age: 33)]]]) - - then:"The result is correct" - renderResult.jsonText == '{"players":[{"age":22},{"age":33}]}' + def renderResult = render( + templateText, + [ + map: [ + players: [ + new PlayerWithAge(name: 'Cantona', age: 22), + new PlayerWithAge(name: 'Giggs', age: 33) + ] + ] + ] + ) + + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "players": [ + { "age": 22 }, + { "age": 33 } + ] + } + ''') } - void "Test render a map type with a simple array"() { - - when:"A map is rendered" + void 'Test render a map type with a simple array'() { + given: def templateText = ''' -model { - Map map -} - -json g.render(map) -''' - def renderResult = render(templateText, [map:[foo:'bar', bar: ['A','B']]]) - - then:"The result is correct" - renderResult.jsonText == '{"foo":"bar","bar":["A","B"]}' - + model { + Map map + } + + json g.render(map) + ''' + + when: 'A map is rendered' + def renderResult = render(templateText, [map: [foo: 'bar', bar: ['A', 'B']]]) + + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + { + "foo": "bar", + "bar": ["A", "B"] + } + ''') } - void "Test render a list of maps"() { - when: + void 'Test render a list of maps'() { + given: def templateText = ''' -model { - List list -} + model { + List list + } + + json g.render(list) + ''' -json g.render(list) -''' - def renderResult = render(templateText, [list:[[foo:'bar', bar: ['A','B']], [x:'y']]]) - - then:"The result is correct" - renderResult.jsonText == '[{"foo":"bar","bar":["A","B"]},{"x":"y"}]' + when: + def renderResult = render(templateText, [list: [[foo: 'bar', bar: ['A', 'B']], [x: 'y']]]) + + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree(''' + [ + { + "foo": "bar", + "bar": ["A", "B"] + }, + { + "x": "y" + } + ] + ''') } - - void "Test render a map with includes"() { - when:"A map is rendered" + void 'Test render a map with includes'() { + given: def templateText = ''' -model { - Map map -} + model { + Map map + } + + json g.render(map, [includes: ['a', 'b']]) + ''' -json g.render(map, [includes: ['a', 'b']]) -''' - def renderResult = render(templateText, [map:[a: "1", b: "2", c: "3"]]) + when: 'A map is rendered' + def renderResult = render(templateText, [map: [a: '1', b: '2', c: '3']]) - then:"The result is correct" - renderResult.jsonText == '{"a":"1","b":"2"}' + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree('{ "a": "1","b": "2" }') - when:"A map is rendered" + when: 'A map is rendered' templateText = ''' -model { - Map map -} - -json g.render(map, [includes: ['a', 'd']]) -''' - renderResult = render(templateText, [map:[a: "1", b: "2", c: "3", d: "4"]]) - - then:"The result is correct" - renderResult.jsonText == '{"a":"1","d":"4"}' + model { + Map map + } + + json g.render(map, [includes: ['a', 'd']]) + ''' + renderResult = render(templateText, [map: [a: '1', b: '2', c: '3', d: '4']]) + + then: 'The result is correct' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree('{ "a": "1", "d": "4" }') } static class PlayerCO implements Validateable { String name String teamName + @SuppressWarnings('unused') static constraints = { - name nullable: false, blank: false - teamName nullable: false, blank: false + name(nullable: false, blank: false) + teamName(nullable: false, blank: false) } } @@ -283,7 +378,4 @@ json g.render(map, [includes: ['a', 'd']]) String name List errors } - - -} - +} \ No newline at end of file diff --git a/json/src/test/groovy/grails/plugin/json/view/NullRenderingSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/NullRenderingSpec.groovy index a28d4914f..ae9c99ba1 100644 --- a/json/src/test/groovy/grails/plugin/json/view/NullRenderingSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/NullRenderingSpec.groovy @@ -1,101 +1,106 @@ package grails.plugin.json.view +import com.fasterxml.jackson.databind.ObjectMapper import grails.plugin.json.view.test.JsonViewTest +import spock.lang.Shared import spock.lang.Specification class NullRenderingSpec extends Specification implements JsonViewTest { - void "test rendering nulls with a domain"() { + @Shared + ObjectMapper objectMapper = new ObjectMapper() + + void 'test rendering nulls with a domain'() { given: def templateText = ''' -import grails.plugin.json.view.* - -model { - Player player -} - -json g.render(player) -''' + import grails.plugin.json.view.* + + model { + Player player + } + + json g.render(player) + ''' when: mappingContext.addPersistentEntity(Player) def renderResult = render(templateText, [player: new Player()]) - then:"No fields are rendered because they are null" + then: 'No fields are rendered because they are null' renderResult.jsonText == '{}' } - void "test rendering nulls with a domain (renderNulls = true)"() { + void 'test rendering nulls with a domain (renderNulls = true)'() { given: def templateText = ''' -import grails.plugin.json.view.* - -model { - Player player -} - -json g.render(player, [renderNulls: true]) -''' + import grails.plugin.json.view.* + + model { + Player player + } + + json g.render(player, [renderNulls: true]) + ''' when: mappingContext.addPersistentEntity(Player) def renderResult = render(templateText, [player: new Player()]) - then:"No fields are rendered because they are null" - renderResult.jsonText == '{"team":null,"name":null}' + then: 'No fields are rendered because they are null' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree('{ "team": null, "name": null}') } - void "test rendering nulls with a map"() { + void 'test rendering nulls with a map'() { given: def templateText = ''' -model { - Map map -} - -json g.render(map) -''' + model { + Map map + } + + json g.render(map) + ''' when: mappingContext.addPersistentEntity(Player) def renderResult = render(templateText, [map: [foo: null, bar: null]]) - then:"Maps with nulls are rendered by default" - renderResult.jsonText == '{"foo":null,"bar":null}' + then: 'Maps with nulls are rendered by default' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree('{"foo": null,"bar": null}') } - void "test rendering nulls with a pogo"() { + void 'test rendering nulls with a pogo'() { given: def templateText = ''' -model { - Object obj -} - -json g.render(obj) -''' + model { + Object obj + } + + json g.render(obj) + ''' when: mappingContext.addPersistentEntity(Player) def renderResult = render(templateText, [obj: new Child2()]) - then:"No fields are rendered because they are null" + then: 'No fields are rendered because they are null' renderResult.jsonText == '{}' } - void "test rendering nulls with a pogo (renderNulls = true)"() { + void 'test rendering nulls with a pogo (renderNulls = true)'() { given: def templateText = ''' -model { - Object obj -} - -json g.render(obj, [renderNulls: true]) -''' + model { + Object obj + } + + json g.render(obj, [renderNulls: true]) + ''' when: mappingContext.addPersistentEntity(Player) def renderResult = render(templateText, [obj: new Child2()]) then: - renderResult.jsonText == '{"name":null,"parent":null}' + objectMapper.readTree(renderResult.jsonText) == objectMapper.readTree('{"name": null, "parent": null}') } -} +} \ No newline at end of file diff --git a/json/src/test/groovy/grails/plugin/json/view/api/JsonApiSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/api/JsonApiSpec.groovy index ccd67095e..e52af88b5 100644 --- a/json/src/test/groovy/grails/plugin/json/view/api/JsonApiSpec.groovy +++ b/json/src/test/groovy/grails/plugin/json/view/api/JsonApiSpec.groovy @@ -1,69 +1,111 @@ package grails.plugin.json.view.api +import com.fasterxml.jackson.databind.ObjectMapper import grails.persistence.Entity import grails.plugin.json.view.test.JsonRenderResult import grails.plugin.json.view.test.JsonViewTest import grails.validation.Validateable -import grails.validation.ValidationErrors import org.grails.testing.GrailsUnitTest +import spock.lang.Shared import spock.lang.Specification class JsonApiSpec extends Specification implements JsonViewTest, GrailsUnitTest { + @Shared + ObjectMapper objectMapper = new ObjectMapper() + void setup() { mappingContext.addPersistentEntities(Widget, Author, Book, ResearchPaper) } void 'test simple case'() { given: - Widget theWidget = new Widget(name: 'One', width: 4, height: 7) - theWidget.id = 5 + def theWidget = new Widget(name: 'One', width: 4, height: 7) + theWidget.id = 5 when: - def result = render(''' -import grails.plugin.json.view.api.Widget -model { - Widget widget -} - -json jsonapi.render(widget) -''', [widget: theWidget]) + def result = render(''' + import grails.plugin.json.view.api.Widget + model { + Widget widget + } + + json jsonapi.render(widget) + ''', [widget: theWidget]) then: - result.jsonText == '''{"data":{"type":"widget","id":"5","attributes":{"width":4,"height":7,"name":"One"}},"links":{"self":"/widget/5"}}''' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "widget", + "id": "5", + "attributes": { + "width": 4, + "height": 7, + "name": "One" + } + }, + "links": { + "self": "/widget/5" + } + } + ''') } void 'test Relationships - hasOne'() { given: - Book returnOfTheKing = new Book( - title: 'The Return of the King', - author: new Author(name: "J.R.R. Tolkien") - ) - returnOfTheKing.id = 3 - returnOfTheKing.author.id = 9 - + Book returnOfTheKing = new Book( + title: 'The Return of the King', + author: new Author(name: 'J.R.R. Tolkien') + ) + returnOfTheKing.id = 3 + returnOfTheKing.author.id = 9 when: - JsonRenderResult result = render(''' -import grails.plugin.json.view.api.Book -model { - Book book -} - -json jsonapi.render(book) -''', [book: returnOfTheKing]) + def result = render(''' + import grails.plugin.json.view.api.Book + model { + Book book + } + + json jsonapi.render(book) + ''', [book: returnOfTheKing]) then: 'The JSON relationships are in place' - result.jsonText == '{"data":{"type":"book","id":"3","attributes":{"title":"The Return of the King"},"relationships":{"author":{"links":{"self":"/author/9"},"data":{"type":"author","id":"9"}}}},"links":{"self":"/book/3"}}' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "book", + "id": "3", + "attributes": { + "title": "The Return of the King" + }, + "relationships": { + "author": { + "links": { + "self": "/author/9" + }, + "data": { + "type": "author", + "id": "9" + } + } + } + }, + "links": { + "self": "/book/3" + } + } + ''') } void 'test Relationships - multiple'() { given: ResearchPaper returnOfTheKing = new ResearchPaper( title: 'The Return of the King', - leadAuthor: new Author(name: "J.R.R. Tolkien"), - coAuthor: new Author(name: "Sally Jones"), - subAuthors: [new Author(name: "Will"), new Author(name: "Smith")] + leadAuthor: new Author(name: 'J.R.R. Tolkien'), + coAuthor: new Author(name: 'Sally Jones'), + subAuthors: [new Author(name: 'Will'), new Author(name: 'Smith')] ) returnOfTheKing.id = 3 returnOfTheKing.leadAuthor.id = 9 @@ -71,134 +113,290 @@ json jsonapi.render(book) returnOfTheKing.subAuthors[0].id = 12 returnOfTheKing.subAuthors[1].id = 13 - when: JsonRenderResult result = render(''' -import grails.plugin.json.view.api.ResearchPaper -model { - ResearchPaper researchPaper -} - -json jsonapi.render(researchPaper) -''', [researchPaper: returnOfTheKing]) + import grails.plugin.json.view.api.ResearchPaper + model { + ResearchPaper researchPaper + } + + json jsonapi.render(researchPaper) + ''', [researchPaper: returnOfTheKing]) then: 'The JSON relationships are in place' - result.jsonText == '{"data":{"type":"researchPaper","id":"3","attributes":{"title":"The Return of the King"},"relationships":{"subAuthors":{"data":[{"type":"author","id":"12"},{"type":"author","id":"13"}]},"leadAuthor":{"links":{"self":"/author/9"},"data":{"type":"author","id":"9"}},"coAuthor":{"links":{"self":"/author/10"},"data":{"type":"author","id":"10"}}}},"links":{"self":"/researchPaper/3"}}' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "researchPaper", + "id": "3", + "attributes": { + "title": "The Return of the King" + }, + "relationships": { + "subAuthors": { + "data": [ + { "type": "author", "id": "12" }, + { "type": "author", "id": "13" } + ] + }, + "leadAuthor": { + "links": { + "self": "/author/9" + }, + "data": { + "type": "author", + "id": "9" + } + }, + "coAuthor": { + "links": { + "self": "/author/10" + }, + "data": { + "type": "author", + "id": "10" + } + } + } + }, + "links": { + "self": "/researchPaper/3" + } + } + ''') } void 'test errors'() { given: - SuperHero mutepool = new SuperHero() - mutepool.name = "" - mutepool.validate() + def mutepool = new SuperHero() + mutepool.name = "" + mutepool.validate() when: - def result = render(''' -import grails.plugin.json.view.api.SuperHero -model { - SuperHero hero -} - -json jsonapi.render(hero) -''', [hero: mutepool]) + def result = render(''' + import grails.plugin.json.view.api.SuperHero + model { + SuperHero hero + } + + json jsonapi.render(hero) + ''', [hero: mutepool]) then: - result.jsonText == '''{"errors":[{"code":"blank","detail":"Property [name] of class [class grails.plugin.json.view.api.SuperHero] cannot be blank","source":{"object":"grails.plugin.json.view.api.SuperHero","field":"name","rejectedValue":"","bindingError":false}}]}''' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "errors": [ + { + "code": "blank", + "detail": "Property [name] of class [class grails.plugin.json.view.api.SuperHero] cannot be blank", + "source": { + "object": "grails.plugin.json.view.api.SuperHero", + "field": "name", + "rejectedValue": "", + "bindingError": false + } + } + ] + } + ''') } void 'test jsonapi object'() { given: - Widget theWidget = new Widget(name: 'One', width: 4, height: 7) - theWidget.id = 5 + def theWidget = new Widget(name: 'One', width: 4, height: 7) + theWidget.id = 5 when: - def result = render(''' -import grails.plugin.json.view.api.Widget -model { - Widget widget -} - -json jsonapi.render(widget, [jsonApiObject: true]) -''', [widget: theWidget]) + def result = render(''' + import grails.plugin.json.view.api.Widget + model { + Widget widget + } + + json jsonapi.render(widget, [jsonApiObject: true]) + ''', [widget: theWidget]) then: - result.jsonText == '''{"jsonapi":{"version":"1.0"},"data":{"type":"widget","id":"5","attributes":{"width":4,"height":7,"name":"One"}},"links":{"self":"/widget/5"}}''' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "jsonapi": { + "version": "1.0" + }, + "data": { + "type": "widget", + "id": "5", + "attributes": { + "width": 4, + "height": 7, + "name": "One" + } + }, + "links": { + "self": "/widget/5" + } + } + ''') } void 'test compound documents object'() { given: - Book returnOfTheKing = new Book( - title: 'The Return of the King', - author: new Author(name: "J.R.R. Tolkien") - ) - returnOfTheKing.id = 3 - returnOfTheKing.author.id = 9 - + def returnOfTheKing = new Book( + title: 'The Return of the King', + author: new Author(name: 'J.R.R. Tolkien') + ) + returnOfTheKing.id = 3 + returnOfTheKing.author.id = 9 when: - JsonRenderResult result = render(''' -import grails.plugin.json.view.api.Book -model { - Book book -} - -json jsonapi.render(book, [expand: 'author']) -''', [book: returnOfTheKing]) + def result = render(''' + import grails.plugin.json.view.api.Book + model { + Book book + } + + json jsonapi.render(book, [expand: 'author']) + ''', [book: returnOfTheKing]) then: 'The JSON relationships are in place' - result.jsonText == '{"data":{"type":"book","id":"3","attributes":{"title":"The Return of the King"},"relationships":{"author":{"links":{"self":"/author/9"},"data":{"type":"author","id":"9"}}}},"links":{"self":"/book/3"},"included":[{"type":"author","id":"9","attributes":{"name":"J.R.R. Tolkien"},"links":{"self":"/author/9"}}]}' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "data": { + "type": "book", + "id": "3", + "attributes": { + "title": "The Return of the King" + }, + "relationships": { + "author": { + "links": { + "self": "/author/9" + }, + "data": { + "type": "author", + "id": "9" + } + } + } + }, + "links": { + "self": "/book/3" + }, + "included": [ + { + "type": "author", + "id": "9", + "attributes": { + "name": "J.R.R. Tolkien" + }, + "links": { + "self": "/author/9" + } + } + ] + } + ''') } - void "test meta object rendering with jsonApiObject"() { + void 'test meta object rendering with jsonApiObject'() { given: - Widget theWidget = new Widget(name: 'One', width: 4, height: 7) + def theWidget = new Widget(name: 'One', width: 4, height: 7) theWidget.id = 5 - def meta = [copyright: "Copyright 2015 Example Corp.", - authors: [ - "Yehuda Katz", - "Steve Klabnik" - ]] + def meta = [ + copyright: 'Copyright 2015 Example Corp.', + authors: [ + 'Yehuda Katz', + 'Steve Klabnik' + ] + ] when: def result = render(''' -import grails.plugin.json.view.api.Widget -model { - Widget widget - Object meta -} - -json jsonapi.render(widget, [jsonApiObject: true, meta: meta]) -''', [widget: theWidget, meta: meta]) + import grails.plugin.json.view.api.Widget + model { + Widget widget + Object meta + } + + json jsonapi.render(widget, [jsonApiObject: true, meta: meta]) + ''', [widget: theWidget, meta: meta]) then: - result.jsonText == '''{"jsonapi":{"version":"1.0","meta":{"copyright":"Copyright 2015 Example Corp.","authors":["Yehuda Katz","Steve Klabnik"]}},"data":{"type":"widget","id":"5","attributes":{"width":4,"height":7,"name":"One"}},"links":{"self":"/widget/5"}}''' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "jsonapi": { + "version": "1.0", + "meta": { + "copyright": "Copyright 2015 Example Corp.", + "authors": [ + "Yehuda Katz", + "Steve Klabnik" + ] + } + }, + "data": { + "type": "widget", + "id": "5", + "attributes": { + "width": 4, + "height": 7, + "name": "One" + } + }, + "links": { + "self": "/widget/5" + } + } + ''') } - void "test meta object rendering without jsonApiObject"() { + void 'test meta object rendering without jsonApiObject'() { given: - Widget theWidget = new Widget(name: 'One', width: 4, height: 7) + def theWidget = new Widget(name: 'One', width: 4, height: 7) theWidget.id = 5 - def meta = [copyright: "Copyright 2015 Example Corp.", - authors: [ - "Yehuda Katz", - "Steve Klabnik" - ]] + def meta = [ + copyright: 'Copyright 2015 Example Corp.', + authors: [ + 'Yehuda Katz', + 'Steve Klabnik' + ] + ] when: def result = render(''' -import grails.plugin.json.view.api.Widget -model { - Widget widget - Object meta -} - -json jsonapi.render(widget, [meta: meta]) -''', [widget: theWidget, meta: meta]) + import grails.plugin.json.view.api.Widget + model { + Widget widget + Object meta + } + + json jsonapi.render(widget, [meta: meta]) + ''', [widget: theWidget, meta: meta]) then: - result.jsonText == '''{"meta":{"copyright":"Copyright 2015 Example Corp.","authors":["Yehuda Katz","Steve Klabnik"]},"data":{"type":"widget","id":"5","attributes":{"width":4,"height":7,"name":"One"}},"links":{"self":"/widget/5"}}''' + objectMapper.readTree(result.jsonText) == objectMapper.readTree(''' + { + "meta": { + "copyright": "Copyright 2015 Example Corp.", + "authors": [ + "Yehuda Katz", + "Steve Klabnik" + ] + }, + "data": { + "type": "widget", + "id": "5", + "attributes": { + "width": 4, + "height": 7, + "name": "One" + } + }, + "links": { + "self": "/widget/5" + } + } + ''') } - } @Entity @@ -217,6 +415,8 @@ class Book { @Entity class ResearchPaper { String title + + @SuppressWarnings('unused') static hasMany = [subAuthors: Author] Author leadAuthor Author coAuthor @@ -230,8 +430,8 @@ class Author { class SuperHero implements Validateable { String name + @SuppressWarnings('unused') static constraints = { name(blank: false) } -} - +} \ No newline at end of file diff --git a/markup/build.gradle b/markup/build.gradle index a4f4d40ff..6ae5b08cb 100644 --- a/markup/build.gradle +++ b/markup/build.gradle @@ -20,7 +20,7 @@ dependencies { implementation libs.spring.beans implementation libs.spring.boot // For @ConfigurationProperties - compileOnly libs.javax.annotation.api // Provided + compileOnly libs.jakarta.annotation.api // Provided testImplementation libs.grails.web.urlmappings testImplementation libs.spock.core diff --git a/markup/src/main/groovy/grails/plugin/markup/view/mvc/MarkupViewResolver.groovy b/markup/src/main/groovy/grails/plugin/markup/view/mvc/MarkupViewResolver.groovy index 04ee5c696..669516dda 100644 --- a/markup/src/main/groovy/grails/plugin/markup/view/mvc/MarkupViewResolver.groovy +++ b/markup/src/main/groovy/grails/plugin/markup/view/mvc/MarkupViewResolver.groovy @@ -10,7 +10,7 @@ import grails.views.mvc.SmartViewResolver import grails.web.mime.MimeType import groovy.transform.CompileStatic import org.springframework.beans.factory.annotation.Autowired -import javax.annotation.PostConstruct +import jakarta.annotation.PostConstruct /** * @author Graeme Rocher