This is an automated email from the ASF dual-hosted git repository. sbglasius pushed a commit to branch fix/issue_15228-respond-errors in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 91c8b395673ffd1b4444f8b8b8da778b3bc1c7e0 Author: Søren Berg Glasius <[email protected]> AuthorDate: Mon Nov 17 16:44:28 2025 +0100 fix: Re-implemented ContainerRenderer on AbstractJsonViewContainerRenderer * Re-enabled gson view resolver * `ContainerRenderer` extends `Renderer` fix to generics parameter * - consequence fix, that `Renderer` calls `render(T render, ...` changed to `render(Object render, ...` and thus contract changed back to pre Grails 7.0.x * New test project to verify outcome. --- .../grails/rest/render/ContainerRenderer.groovy | 2 +- .../main/groovy/grails/rest/render/Renderer.groovy | 2 +- .../rest/render/errors/VndErrorJsonRenderer.groovy | 3 +- .../rest/render/errors/VndErrorXmlRenderer.groovy | 3 +- .../grails/rest/render/hal/HalJsonRenderer.groovy | 2 +- .../render/util/AbstractLinkingRenderer.groovy | 4 +- .../rest/render/json/DefaultJsonRenderer.groovy | 2 +- grails-test-examples/issue-15228/build.gradle | 48 ++++++++++ .../issue-15228/grails-app/conf/application.yml | 58 ++++++++++++ .../issue-15228/grails-app/conf/logback.xml | 39 ++++++++ .../issue15228/app/AppController.groovy | 33 ++++--- .../controllers/issue15228/app/UrlMappings.groovy | 21 ++--- .../init/issue15228/app/Application.groovy | 22 ++--- .../grails-app/views/app/normalView.gson | 11 +++ .../grails-app/views/errors/_errors.gson | 21 +++++ .../_otherValidateableObject.gson | 11 +++ .../issue11767/app/GsonViewRespondSpec.groovy | 104 +++++++++++++++++++++ .../issue15228/app/OtherValidateableObject.groovy | 11 +++ .../issue15228/app/ValidateableObject.groovy | 11 +++ .../groovy/functional/tests/BookSpec.groovy | 32 +++---- .../views/mvc/renderer/DefaultViewRenderer.groovy | 2 +- .../AbstractJsonViewContainerRenderer.groovy | 11 +-- settings.gradle | 2 + 23 files changed, 381 insertions(+), 74 deletions(-) diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy b/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy index 4f1c612f19..e9ef4b2a4c 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy +++ b/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy @@ -24,7 +24,7 @@ package grails.rest.render * @author Graeme Rocher * @since 2.3 */ -interface ContainerRenderer<C, T> extends Renderer<C> { +interface ContainerRenderer<C, T> extends Renderer<T> { /** * @return The underlying type wrapped by the container. For example with List<Book>, this method would return Book diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/Renderer.groovy b/grails-rest-transforms/src/main/groovy/grails/rest/render/Renderer.groovy index 39c4ee06b1..1b8a93470b 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/Renderer.groovy +++ b/grails-rest-transforms/src/main/groovy/grails/rest/render/Renderer.groovy @@ -39,5 +39,5 @@ interface Renderer<T> extends MimeTypeProvider { * @param object The object to render * @param context The {@link RenderContext} */ - void render(T object, RenderContext context) + void render(Object object, RenderContext context) } diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorJsonRenderer.groovy b/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorJsonRenderer.groovy index 4fcd5642e9..df0882056d 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorJsonRenderer.groovy +++ b/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorJsonRenderer.groovy @@ -25,7 +25,6 @@ import groovy.transform.CompileStatic import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.validation.BeanPropertyBindingResult -import org.springframework.validation.Errors import org.springframework.validation.ObjectError import grails.rest.render.RenderContext @@ -48,7 +47,7 @@ class VndErrorJsonRenderer extends AbstractVndErrorRenderer { MimeType[] mimeTypes = [MIME_TYPE, MimeType.HAL_JSON, MimeType.JSON, MimeType.TEXT_JSON] as MimeType[] @Override - void render(Errors object, RenderContext context) { + void render(Object object, RenderContext context) { if (messageSource == null) throw new IllegalStateException('messageSource property null') if (object instanceof BeanPropertyBindingResult) { diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorXmlRenderer.groovy b/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorXmlRenderer.groovy index 1e5d9f52b8..d5516a0686 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorXmlRenderer.groovy +++ b/grails-rest-transforms/src/main/groovy/grails/rest/render/errors/VndErrorXmlRenderer.groovy @@ -22,7 +22,6 @@ import groovy.transform.CompileStatic import org.springframework.http.HttpStatus import org.springframework.validation.BeanPropertyBindingResult -import org.springframework.validation.Errors import org.springframework.validation.ObjectError import grails.rest.render.RenderContext @@ -50,7 +49,7 @@ class VndErrorXmlRenderer extends AbstractVndErrorRenderer { MimeType[] mimeTypes = [MIME_TYPE, MimeType.HAL_XML, MimeType.XML, MimeType.TEXT_XML] as MimeType[] @Override - void render(Errors object, RenderContext context) { + void render(Object object, RenderContext context) { if (object instanceof BeanPropertyBindingResult) { def errors = object as BeanPropertyBindingResult context.setContentType(GrailsWebUtil.getContentType(MIME_TYPE.name, encoding)) diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/hal/HalJsonRenderer.groovy b/grails-rest-transforms/src/main/groovy/grails/rest/render/hal/HalJsonRenderer.groovy index 2b5ff38129..d8e5ffb969 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/hal/HalJsonRenderer.groovy +++ b/grails-rest-transforms/src/main/groovy/grails/rest/render/hal/HalJsonRenderer.groovy @@ -108,7 +108,7 @@ class HalJsonRenderer<T> extends AbstractLinkingRenderer<T> { } @Override - void renderInternal(T object, RenderContext context) { + void renderInternal(Object object, RenderContext context) { final mimeType = context.acceptMimeType ?: mimeTypes[0] final responseWriter = context.writer Writer targetWriter = prettyPrint ? new StringWriter() : responseWriter diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/util/AbstractLinkingRenderer.groovy b/grails-rest-transforms/src/main/groovy/grails/rest/render/util/AbstractLinkingRenderer.groovy index 1428d23ae0..e78a82644f 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/util/AbstractLinkingRenderer.groovy +++ b/grails-rest-transforms/src/main/groovy/grails/rest/render/util/AbstractLinkingRenderer.groovy @@ -107,7 +107,7 @@ abstract class AbstractLinkingRenderer<T> extends AbstractIncludeExcludeRenderer } @Override - void render(T object, RenderContext renderContext) { + void render(Object object, RenderContext renderContext) { def mimeType = renderContext.acceptMimeType ?: getMimeTypes()[0] def contentType = GrailsWebUtil.getContentType(mimeType.name, encoding) renderContext.setContentType(contentType) @@ -123,7 +123,7 @@ abstract class AbstractLinkingRenderer<T> extends AbstractIncludeExcludeRenderer } } - abstract void renderInternal(T object, RenderContext context) + abstract void renderInternal(Object object, RenderContext context) protected boolean isDomainResource(Class clazz) { if (mappingContext != null) { diff --git a/grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy b/grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy index 429f75c515..562dbc878b 100644 --- a/grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy +++ b/grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy @@ -76,7 +76,7 @@ class DefaultJsonRenderer<T> implements Renderer<T> { } @Override - void render(T object, RenderContext context) { + void render(Object object, RenderContext context) { final mimeType = context.acceptMimeType ?: MimeType.JSON context.setContentType(GrailsWebUtil.getContentType(mimeType.name, encoding)) def viewName = context.viewName ?: context.actionName diff --git a/grails-test-examples/issue-15228/build.gradle b/grails-test-examples/issue-15228/build.gradle new file mode 100644 index 0000000000..8d586c75a6 --- /dev/null +++ b/grails-test-examples/issue-15228/build.gradle @@ -0,0 +1,48 @@ +/* + * 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. + */ + +version = '0.1' +group = 'issue11767.app' + +apply plugin: 'org.apache.grails.gradle.grails-web' + +dependencies { + implementation platform(project(':grails-bom')) + + implementation 'org.springframework.boot:spring-boot-starter-logging' + implementation 'org.apache.grails:grails-dependencies-starter-web' + + implementation 'org.apache.grails:grails-views-gson' + implementation 'org.apache.grails:grails-rest-transforms' + + testAndDevelopmentOnly platform(project(':grails-bom')) + + testImplementation 'org.apache.grails:grails-testing-support-views-gson' + testImplementation 'org.apache.grails.testing:grails-testing-support-core' + + integrationTestImplementation 'com.fasterxml.jackson.core:jackson-databind' + integrationTestImplementation "io.micronaut:micronaut-http-client:$micronautHttpClientVersion" + integrationTestImplementation "io.micronaut:micronaut-jackson-databind:$micronautHttpClientVersion" +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/java-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle') +} diff --git a/grails-test-examples/issue-15228/grails-app/conf/application.yml b/grails-test-examples/issue-15228/grails-app/conf/application.yml new file mode 100644 index 0000000000..af9d03596e --- /dev/null +++ b/grails-test-examples/issue-15228/grails-app/conf/application.yml @@ -0,0 +1,58 @@ +# 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. + +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +grails: + mime: + disable: + accept: + header: + userAgents: + - Gecko + - WebKit + - Presto + - Trident + types: + all: '*/*' + atom: application/atom+xml + css: text/css + csv: text/csv + form: application/x-www-form-urlencoded + html: + - text/html + - application/xhtml+xml + js: text/javascript + json: + - application/json + - text/json + multipartForm: multipart/form-data + pdf: application/pdf + rss: application/rss+xml + text: text/plain + hal: + - application/hal+json + - application/hal+xml + xml: + - text/xml + - application/xml +micronaut: + http: + client: + readTimeout: PT10M + \ No newline at end of file diff --git a/grails-test-examples/issue-15228/grails-app/conf/logback.xml b/grails-test-examples/issue-15228/grails-app/conf/logback.xml new file mode 100644 index 0000000000..d64fd5d712 --- /dev/null +++ b/grails-test-examples/issue-15228/grails-app/conf/logback.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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 + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +--> +<configuration> + + <conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter" /> + <conversionRule conversionWord="wex" class="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> + + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <withJansi>true</withJansi> + <encoder> + <charset>UTF-8</charset> + <pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex</pattern> + </encoder> + </appender> + + <root level="info"> + <appender-ref ref="STDOUT" /> + </root> + +</configuration> diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy b/grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/AppController.groovy similarity index 63% copy from grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy copy to grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/AppController.groovy index 4f1c612f19..d7d86b7092 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy +++ b/grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/AppController.groovy @@ -16,18 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -package grails.rest.render -/** - * A container a renderer is a render that renders a container of objects (Example: List of Book instances) - * - * @author Graeme Rocher - * @since 2.3 - */ -interface ContainerRenderer<C, T> extends Renderer<C> { +package issue15228.app + +import org.springframework.http.HttpStatus + +import grails.artefact.Controller +import grails.artefact.controller.RestResponder - /** - * @return The underlying type wrapped by the container. For example with List<Book>, this method would return Book - */ - Class<T> getComponentType() +class AppController implements Controller, RestResponder { + + def normalView(ValidateableObject obj) { + respond(obj) + } + + def typeView(OtherValidateableObject obj) { + respond(obj) + } + + def errorView(ValidateableObject obj) { + respond(obj.errors, status: HttpStatus.UNPROCESSABLE_ENTITY) + } + } + diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy b/grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/UrlMappings.groovy similarity index 66% copy from grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy copy to grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/UrlMappings.groovy index 4f1c612f19..34fe52fd3d 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy +++ b/grails-test-examples/issue-15228/grails-app/controllers/issue15228/app/UrlMappings.groovy @@ -16,18 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -package grails.rest.render -/** - * A container a renderer is a render that renders a container of objects (Example: List of Book instances) - * - * @author Graeme Rocher - * @since 2.3 - */ -interface ContainerRenderer<C, T> extends Renderer<C> { +package issue15228.app - /** - * @return The underlying type wrapped by the container. For example with List<Book>, this method would return Book - */ - Class<T> getComponentType() +class UrlMappings { + static mappings = { + "/$controller/$action?/$id?(.$format)?"{ + constraints { + // apply constraints here + } + } + } } diff --git a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy b/grails-test-examples/issue-15228/grails-app/init/issue15228/app/Application.groovy similarity index 66% copy from grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy copy to grails-test-examples/issue-15228/grails-app/init/issue15228/app/Application.groovy index 4f1c612f19..cb27071abc 100644 --- a/grails-rest-transforms/src/main/groovy/grails/rest/render/ContainerRenderer.groovy +++ b/grails-test-examples/issue-15228/grails-app/init/issue15228/app/Application.groovy @@ -16,18 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package grails.rest.render -/** - * A container a renderer is a render that renders a container of objects (Example: List of Book instances) - * - * @author Graeme Rocher - * @since 2.3 - */ -interface ContainerRenderer<C, T> extends Renderer<C> { +package issue15228.app + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration +import groovy.transform.CompileStatic - /** - * @return The underlying type wrapped by the container. For example with List<Book>, this method would return Book - */ - Class<T> getComponentType() +@CompileStatic +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } } diff --git a/grails-test-examples/issue-15228/grails-app/views/app/normalView.gson b/grails-test-examples/issue-15228/grails-app/views/app/normalView.gson new file mode 100644 index 0000000000..d15b5b2fbc --- /dev/null +++ b/grails-test-examples/issue-15228/grails-app/views/app/normalView.gson @@ -0,0 +1,11 @@ +import issue15228.app.ValidateableObject + +model { + ValidateableObject validateableObject +} + +json { + normal { + foo validateableObject.foo + } +} diff --git a/grails-test-examples/issue-15228/grails-app/views/errors/_errors.gson b/grails-test-examples/issue-15228/grails-app/views/errors/_errors.gson new file mode 100644 index 0000000000..18ee4a5a3a --- /dev/null +++ b/grails-test-examples/issue-15228/grails-app/views/errors/_errors.gson @@ -0,0 +1,21 @@ +import org.springframework.validation.* + +model { + Errors errors +} + +json { + error { + if(errors.globalErrorCount == 1 && errors.fieldErrorCount == 0) { + message messageSource.getMessage(errors.globalError, locale) + } else { + errors(errors.allErrors) { ObjectError error -> + if(error instanceof FieldError) { + field error.field + rejectedValue error.rejectedValue + } + message messageSource.getMessage(error, locale) + } + } + } +} diff --git a/grails-test-examples/issue-15228/grails-app/views/otherValidateableObject/_otherValidateableObject.gson b/grails-test-examples/issue-15228/grails-app/views/otherValidateableObject/_otherValidateableObject.gson new file mode 100644 index 0000000000..480d00f8a3 --- /dev/null +++ b/grails-test-examples/issue-15228/grails-app/views/otherValidateableObject/_otherValidateableObject.gson @@ -0,0 +1,11 @@ +import issue15228.app.OtherValidateableObject + +model { + OtherValidateableObject otherValidateableObject +} + +json { + type { + foo otherValidateableObject.foo + } +} diff --git a/grails-test-examples/issue-15228/src/integration-test/groovy/issue11767/app/GsonViewRespondSpec.groovy b/grails-test-examples/issue-15228/src/integration-test/groovy/issue11767/app/GsonViewRespondSpec.groovy new file mode 100644 index 0000000000..82d8adbea5 --- /dev/null +++ b/grails-test-examples/issue-15228/src/integration-test/groovy/issue11767/app/GsonViewRespondSpec.groovy @@ -0,0 +1,104 @@ +/* + * 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. + */ + +package issue11767.app + + +import com.fasterxml.jackson.databind.ObjectMapper +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import grails.testing.mixin.integration.Integration + +@Integration +class GsonViewRespondSpec extends Specification { + + @Shared + ObjectMapper objectMapper + + def setupSpec() { + objectMapper = new ObjectMapper() + } + + @Shared + @AutoCleanup + HttpClient httpClient + + void setup() { + def baseUrl = "http://localhost:$serverPort" + httpClient = HttpClient.create(baseUrl.toURL()) + } + + void 'respond with Error gson view'() { + when: 'The app controller is visited on errorView' + def request = HttpRequest.GET('/app/errorView?foo=Too+Short').accept(MediaType.APPLICATION_JSON) + httpClient.toBlocking().exchange(request, String) + + then: + def ex = thrown(HttpClientResponseException) + ex.status == HttpStatus.UNPROCESSABLE_ENTITY + + and: + objectMapper.readTree(ex.response.body() as String) == objectMapper.readTree(''' + { + "error": { + "errors": [ + { + "field": "foo", + "rejectedValue": "Too Short", + "message": "Property [foo] of class [class issue15228.app.ValidateableObject] with value [Too Short] is less than the minimum size of [10]" + } + ] + } + }''') + } + + void 'respond with gson view from action name'() { + when: 'The app controller is visited on normalView' + def request = HttpRequest.GET('/app/normalView?foo=Testing+normal+view').accept(MediaType.APPLICATION_JSON) + def response = httpClient.toBlocking().exchange(request, String) + + then: + objectMapper.readTree(response.body() as String) == objectMapper.readTree('''{ + "normal": { + "foo": "Testing normal view" + } + }''') + } + + void 'respond with gson view from type'() { + when: 'The app controller is visited on typeView' + def request = HttpRequest.GET('/app/typeView?foo=Testing+type+view').accept(MediaType.APPLICATION_JSON) + def response = httpClient.toBlocking().exchange(request, String) + + then: + objectMapper.readTree(response.body() as String) == objectMapper.readTree('''{ + "type": { + "foo": "Testing type view" + } + }''') + } + +} diff --git a/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/OtherValidateableObject.groovy b/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/OtherValidateableObject.groovy new file mode 100644 index 0000000000..62e0788239 --- /dev/null +++ b/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/OtherValidateableObject.groovy @@ -0,0 +1,11 @@ +package issue15228.app + +import grails.validation.Validateable + +class OtherValidateableObject implements Validateable { + String foo + + static constraints = { + foo minSize: 10 + } +} \ No newline at end of file diff --git a/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/ValidateableObject.groovy b/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/ValidateableObject.groovy new file mode 100644 index 0000000000..16c918d148 --- /dev/null +++ b/grails-test-examples/issue-15228/src/main/groovy/issue15228/app/ValidateableObject.groovy @@ -0,0 +1,11 @@ +package issue15228.app + +import grails.validation.Validateable + +class ValidateableObject implements Validateable { + String foo + + static constraints = { + foo minSize: 10 + } +} \ No newline at end of file diff --git a/grails-test-examples/views-functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy b/grails-test-examples/views-functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy index 5001b99117..626206fe86 100644 --- a/grails-test-examples/views-functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy +++ b/grails-test-examples/views-functional-tests/src/integration-test/groovy/functional/tests/BookSpec.groovy @@ -34,13 +34,8 @@ import spock.lang.Shared @Integration(applicationClass = Application) class BookSpec extends HttpClientSpec { - @Shared - ObjectMapper objectMapper - - def setupSpec() { - objectMapper = new ObjectMapper() - } + @RunOnce @BeforeEach void init() { @@ -56,22 +51,17 @@ class BookSpec extends HttpClientSpec { HttpClientResponseException e = thrown() e.response.status == HttpStatus.UNPROCESSABLE_ENTITY e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).isPresent() - // This has changed somewhere along the way - // e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/vnd.error;charset=UTF-8' - // to -> - e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/json;charset=UTF-8' - objectMapper.readTree(e.response.body().toString()) == objectMapper.readTree(''' + e.response.headers.getFirst(HttpHeaders.CONTENT_TYPE).get() == 'application/vnd.error;charset=UTF-8' + objectMapper.readTree(e.response.body().toString()) == objectMapper.readTree(""" { - "errors": [ - { - "object": "functional.tests.Book", - "field": "title", - "rejected-value": null, - "message": "Property [title] of class [class functional.tests.Book] cannot be null" - } - ] - } - ''') + "message": "Property [title] of class [class functional.tests.Book] cannot be null", + "path": "/book/index", + "_links": { + "self": { + "href": "$baseUrl/book/index" + } + } + }""") } void 'Test REST view rendering'() { diff --git a/grails-views-core/src/main/groovy/grails/views/mvc/renderer/DefaultViewRenderer.groovy b/grails-views-core/src/main/groovy/grails/views/mvc/renderer/DefaultViewRenderer.groovy index 6543c82837..0e04501f44 100644 --- a/grails-views-core/src/main/groovy/grails/views/mvc/renderer/DefaultViewRenderer.groovy +++ b/grails-views-core/src/main/groovy/grails/views/mvc/renderer/DefaultViewRenderer.groovy @@ -64,7 +64,7 @@ abstract class DefaultViewRenderer<T> extends DefaultHtmlRenderer<T> { } @Override - void render(T object, RenderContext context) { + void render(Object object, RenderContext context) { def arguments = context.arguments def ct = arguments?.contentType diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy index d32b2c9ee6..384cf1c9a7 100644 --- a/grails-views-gson/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy +++ b/grails-views-gson/src/main/groovy/grails/plugin/json/renderer/AbstractJsonViewContainerRenderer.groovy @@ -25,6 +25,7 @@ import groovy.transform.InheritConstructors import org.springframework.beans.factory.annotation.Autowired import grails.plugin.json.view.mvc.JsonViewResolver +import grails.rest.render.ContainerRenderer import grails.rest.render.RenderContext import grails.util.GrailsNameUtils import grails.views.Views @@ -39,13 +40,13 @@ import org.grails.plugins.web.rest.render.json.DefaultJsonRenderer */ @CompileStatic @InheritConstructors -abstract class AbstractJsonViewContainerRenderer<C,T> extends DefaultJsonRenderer<T> { +abstract class AbstractJsonViewContainerRenderer<C,T> extends DefaultJsonRenderer<T> implements ContainerRenderer<C,T> { @Autowired JsonViewResolver jsonViewResolver @Override - void render(T object, RenderContext context) { + void render(Object object, RenderContext context) { if (jsonViewResolver != null) { String viewUri = "/${context.controllerName}/_${GrailsNameUtils.getPropertyName(targetType)}" def webRequest = ((ServletRenderContext) context).getWebRequest() @@ -68,12 +69,10 @@ abstract class AbstractJsonViewContainerRenderer<C,T> extends DefaultJsonRendere def request = webRequest.currentRequest def response = webRequest.currentResponse view.render(model, request, response) - } - else { + } else { super.render(object, context) } - } - else { + } else { super.render(object, context) } } diff --git a/settings.gradle b/settings.gradle index 3e2a209b7d..196ca167c3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -406,6 +406,7 @@ include( 'grails-test-examples-plugins-loadafter', 'grails-test-examples-plugins-issue11005', 'grails-test-examples-issue-11767', + 'grails-test-examples-issue-15228', 'grails-test-examples-plugins-exploded', 'grails-test-examples-plugins-issue-11767', 'grails-test-examples-cache', @@ -438,6 +439,7 @@ project(':grails-test-examples-plugins-loadsecond').projectDir = file('grails-te project(':grails-test-examples-plugins-loadafter').projectDir = file('grails-test-examples/plugins/loadafter') project(':grails-test-examples-plugins-issue11005').projectDir = file('grails-test-examples/plugins/issue11005') project(':grails-test-examples-issue-11767').projectDir = file('grails-test-examples/issue-11767') +project(':grails-test-examples-issue-15228').projectDir = file('grails-test-examples/issue-15228') project(':grails-test-examples-plugins-exploded').projectDir = file('grails-test-examples/plugins/exploded') project(':grails-test-examples-plugins-issue-11767').projectDir = file('grails-test-examples/plugins/issue-11767') project(':grails-test-examples-cache').projectDir = file('grails-test-examples/cache')
