codeconsole commented on code in PR #16237: URL: https://github.com/apache/grails-core/pull/16237#discussion_r4249265668
########## grails-mimetypes/src/main/groovy/org/grails/web/mime/GrailsContentNegotiationStrategy.groovy: ########## @@ -0,0 +1,127 @@ +/* + * 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 org.grails.web.mime + +import java.util.regex.Pattern + +import groovy.transform.CompileStatic + +import jakarta.servlet.http.HttpServletRequest + +import org.springframework.http.MediaType +import org.springframework.web.accept.ContentNegotiationStrategy +import org.springframework.web.context.request.NativeWebRequest + +import grails.config.Config +import grails.config.Settings +import grails.web.http.HttpHeaders +import grails.web.mime.MimeType +import org.grails.web.util.GrailsApplicationAttributes +import org.grails.web.util.WebUtils + +/** + * Adapts Grails' configured format aliases and compatibility rules to Spring MVC content negotiation. + */ +@CompileStatic +class GrailsContentNegotiationStrategy implements ContentNegotiationStrategy { + + private final MimeType[] mimeTypes + private final boolean useAcceptHeader + private final boolean useAcceptHeaderXhr + private final Pattern disableForUserAgents + + GrailsContentNegotiationStrategy(MimeType[] mimeTypes, Config config) { + this.mimeTypes = mimeTypes + this.useAcceptHeader = config.getProperty(Settings.MIME_USE_ACCEPT_HEADER, Boolean, true) + this.useAcceptHeaderXhr = !config.getProperty( + Settings.MIME_DISABLE_ACCEPT_HEADER_FOR_USER_AGENTS_XHR, + Boolean, + false + ) + this.disableForUserAgents = createDisabledUserAgentPattern(config) + } + + @Override + List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest) + if (request == null) { + return [MediaType.ALL] + } + return resolveMimeTypes(request).collect { MimeType mimeType -> + SpringMediaTypeAdapter.toMediaType(mimeType) + } + } + + /** + * @return the MIME types this strategy resolves against, as configured by grails.mime.types + */ + MimeType[] getConfiguredMimeTypes() { + return mimeTypes + } + + MimeType[] resolveMimeTypes(HttpServletRequest request) { + String formatOverride + if (!WebUtils.isError(request)) { + formatOverride = request.getParameter('format') + } + if (!formatOverride) { + formatOverride = request.getAttribute(GrailsApplicationAttributes.RESPONSE_FORMAT) as String + } + if (formatOverride) { + MimeType mimeType = mimeTypes.find { MimeType candidate -> candidate.extension == formatOverride } + return [mimeType ?: mimeTypes[0]] as MimeType[] Review Comment: Fixed in 173e94f9b8. `response.mimeTypes` again resolves only the Accept header, through the strategy's new `resolveAcceptedMimeTypes`, so `withFormat` falls back to it as on 8.0.x. `response.format`, the format-aware list and Spring's `resolveMediaTypes` still apply the override. `WithFormatSpec` 'a known format without a block of its own falls back to the Accept header' runs your case (`?format=xml`, `Accept: text/html`, `html` and `json` blocks) and gets the `html` block. The last two paragraphs of section 8 are gone. ########## grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/DefaultRendererRegistry.groovy: ########## @@ -125,6 +156,8 @@ class DefaultRendererRegistry extends ClassAndMimeTypeRegistry<Renderer, Rendere if (renderer instanceof ContainerRenderer) { ContainerRenderer cr = (ContainerRenderer) renderer addContainerRenderer(cr.componentType, cr) + } else if (renderer instanceof FallbackRenderer) { Review Comment: Fixed in b355586f58. - `addRenderer` runs every `DefaultJsonRenderer` through the registry's configuration. It fills only what the bean leaves unset, and the bean shares the registry's one legacy warning. `useSpringJson` is now null by default, so a renderer that sets it keeps its choice. - `DefaultXmlRenderer` resolves `SpringMessageConverters` through an autowired provider when the plugin has not supplied the converters. - Tests: `ControllerJsonSerializationSpec` 'an application JsonRenderer bean follows the Spring JSON setting' covers a `JsonRenderer` bean with the setting on, including `jsonConfiguration:`. `ControllerUnitTestMessageConvertersSpec` covers an `XmlRenderer` bean. `DefaultRendererRegistrySpec` checks that an explicit `useSpringJson = false` is kept. - Section 2 says renderer beans follow the settings. ########## grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/json/DefaultJsonRenderer.groovy: ########## @@ -108,20 +160,164 @@ class DefaultJsonRenderer<T> implements Renderer<T> { * @param context */ protected void renderJson(T object, RenderContext context) { + String selectedConfiguration = context.arguments?.get('jsonConfiguration')?.toString() + if (selectedConfiguration) { + if (namedJsonRenderer == null || !namedJsonRenderer.contains(selectedConfiguration)) { + throw new IllegalArgumentException("Named JSON configuration [$selectedConfiguration] is not registered.") + } + namedJsonRenderer.render(selectedConfiguration, object, context.writer, + context.includes, context.excludes) + return + } + if (object instanceof ProblemDetail || canUseSpringConverter(context)) { + Object springValue = object + if (object instanceof Errors) { + springValue = validationProblemDetailFactory.create((Errors) object, errorsHttpStatus) + } + MediaType mediaType = springValue instanceof ProblemDetail ? + MediaType.parseMediaType(PROBLEM_JSON.name) : + MediaType.parseMediaType(resolveMimeType(context).name) + // Set the content type before writing: once the writer flushes, the response is + // committed and a later content type change is silently discarded. + if (springValue instanceof ProblemDetail) { + ProblemDetail problem = (ProblemDetail) springValue + context.setStatus(HttpStatus.valueOf(problem.status)) Review Comment: Fixed in a4a5f1c45a. The status is resolved with `HttpStatus.resolve`. A code without a constant is written to the servlet response directly; a non-servlet `RenderContext` gets an `IllegalArgumentException` that names it. `DefaultJsonRendererSpec` runs 422, 499, 299 and 599. Section 6 now says that a `ProblemDetail` takes the Spring path whatever `grails.web.rendering.json.spring` is set to, so the example answers with `application/problem+json` in Grails 9. ########## grails-web-databinding/src/main/groovy/org/grails/web/databinding/bindingsource/JsonDataBindingSourceCreator.groovy: ########## @@ -44,12 +55,58 @@ import org.grails.web.json.JSONObject * @see org.grails.databinding.bindingsource.DataBindingSourceCreator */ @CompileStatic +@Slf4j class JsonDataBindingSourceCreator extends AbstractRequestBodyDataBindingSourceCreator { private static final Pattern INDEX_PATTERN = ~/^(\S+)\[(\d+)\]$/ + // Resolved when a request body is first parsed rather than injected. Injecting it pulls + // Jackson's auto-configuration into this bean's graph, and MimeTypesConfiguration depends on + // this creator, so Boot's mapper would be built before GORM has initialized. + @Autowired(required = false) + ObjectProvider<JsonMapper> jsonMapperProvider + @Autowired(required = false) - JsonSlurper jsonSlurper = new JsonSlurper() + ApplicationContext applicationContext + + @Value('${grails.databinding.json.jackson:false}') + boolean useJackson = false + + @PostConstruct Review Comment: Both fixed in 4efb6473e8. The notice is logged once per application, using a flag that Grails' shutdown operations reset. `@Autowired(required = false) JsonSlurper jsonSlurper = new JsonSlurper()` is back and used on the legacy path. `JsonDataBindingSourceCreatorSpec` checks for one notice across the JSON, HAL and JSON API creators, and that a LAX `JsonSlurper` bean parses `{name: Grails}`. Section 5 mentions the bean. ########## grails-doc/src/en/guide/upgrading/upgrading90x.adoc: ########## @@ -0,0 +1,333 @@ +//// +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. +//// + +=== Upgrade Instructions for Grails and Related Dependencies + +This guide outlines the changes to review when you upgrade a Grails project from Grails 8 to Grails 9. + +==== 1. XML Web Support Moves to a Separate Module + +XML conversion, request-body binding, codecs, and REST renderers now live in the optional +`grails-xml` module. The web starter still includes it in Grails 9, preserving existing XML endpoints. +JSON-only applications can exclude `org.apache.grails:grails-xml` from their starter dependency now. +In Grails 10 the starter will stop including it and `@Resource` will default to JSON only. +Legacy XML conversion logs one deprecation warning on use and is scheduled for removal in Grails 11. Review Comment: Done in ac485a7092. - `HalXmlRenderer`, `HalXmlCollectionRenderer`, `VndErrorXmlRenderer` and the XML `ValidationErrorsMarshaller` are `forRemoval = true` with "Scheduled for removal in Grails 11", which `XmlCompatibilitySpec` asserts. - `hal.adoc` and `vndError.adoc` name Grails 11. - `defaultRenderers.adoc`, `restControllersStepByStep.adoc`, `domainResources.adoc` (both places), `RSSAndAtom.adoc` and `codecs.adoc` now say which parts need `grails-xml`. ########## grails-doc/src/en/guide/upgrading/upgrading90x.adoc: ########## @@ -0,0 +1,333 @@ +//// +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. +//// + +=== Upgrade Instructions for Grails and Related Dependencies + +This guide outlines the changes to review when you upgrade a Grails project from Grails 8 to Grails 9. + +==== 1. XML Web Support Moves to a Separate Module + +XML conversion, request-body binding, codecs, and REST renderers now live in the optional +`grails-xml` module. The web starter still includes it in Grails 9, preserving existing XML endpoints. +JSON-only applications can exclude `org.apache.grails:grails-xml` from their starter dependency now. +In Grails 10 the starter will stop including it and `@Resource` will default to JSON only. +Legacy XML conversion logs one deprecation warning on use and is scheduled for removal in Grails 11. +MIME type negotiation still recognizes `application/xml`, `text/xml`, Atom, and other XML media +types; recognizing a media type does not install an XML serializer. + +New REST controllers, RESTful controllers, resources, and scaffolded controllers generated by the +`rest-api` profile advertise JSON only. After adding `grails-xml`, add `xml` to the artefact's +`responseFormats` or `formats` declaration when that endpoint should negotiate XML. + +Applications that expose XML endpoints or use `grails.converters.XML`, XML object marshallers, XML data +binding, or the XML and Atom renderers need this module. It is supplied transitively by the Grails 9 +web starter; applications without that starter, and applications upgrading to Grails 10, add it explicitly: + +[source,groovy] +.build.gradle +---- +dependencies { + implementation 'org.apache.grails:grails-xml' +} +---- + +The module preserves the established Grails XML APIs and compatibility marshalling for domain-shaped +payloads, collections, maps, validation errors, named converter configurations, and include/exclude +projections. + +Set `grails.web.rendering.xml.spring: true` to write ordinary bean responses with Spring Framework's `JacksonXmlHttpMessageConverter`, +backed by the Spring Boot-managed Jackson XML mapper. As in a plain Spring Boot application, that +converter is registered only when Jackson's XML dataformat is on the classpath, so applications that +want it add the dependency themselves: + +[source,groovy] +.build.gradle +---- +dependencies { + runtimeOnly 'tools.jackson.dataformat:jackson-dataformat-xml' +} +---- + +Grails 9 defaults this setting to `false`: adding the Jackson XML dependency alone does not bypass +registered XML marshallers. Enabling it explicitly selects Spring for ordinary beans, even when a +legacy marshaller is registered. Domain objects, collections, maps, validation errors and projections +continue to use the Grails XML converter; migrate these to DTOs or explicit application renderers +before legacy XML conversion is removed in Grails 11. + +Without the Jackson XML dependency, `grails-xml` still renders XML through the Grails converter. Applications may also register +their own Spring XML message converter or OXM/JAXB infrastructure through public Spring configuration +APIs. + +This change does not affect Grails plugin descriptors. Plugin metadata continues to be generated and +published in its existing descriptor format; `grails-xml` concerns application HTTP payloads only. + +==== 2. Opt Into Spring JSON Conversion + +Grails 9 preserves the legacy `grails.converters.JSON` converter for `respond()` by default and logs one +deprecation warning when that path renders. Opt into Spring MVC's message converters, backed by Boot's +configured Jackson `JsonMapper`, with `grails.web.rendering.json.spring: true`. Grails 10 will enable it +by default; the legacy `respond()` path is scheduled for removal in Grails 11. Jackson can render date +formats, bean properties, circular references, and validation errors differently, so review the JSON +returned by `respond()` endpoints when you upgrade. Explicit `render value as JSON` calls still use the +legacy converter. + +The setting alone determines the default response path: registering a marshaller does not implicitly +change it. `false` retains the legacy converter (with the warning); `true` uses Spring's converters +even while legacy marshallers are registered. Migrate those representations to Jackson serializers +before enabling it. Legacy pretty-print settings do not configure Jackson; use +`spring.jackson.serialization.indent-output` on the Spring path. + +[source,yaml] +.application.yml +---- +grails: + web: + rendering: + json: + spring: true +---- + +Grails selects the first configured MVC `HttpMessageConverter` that can write the +response type and negotiated JSON media type and advertises a JSON media type for that class +(`application/json`, `text/json`, or a `+json` subtype). Generic `*/*` string and byte-array converters +are skipped, so strings remain JSON strings and byte arrays use Jackson's base64 JSON representation. +The same media-family rule applies to XML conversion; a generic text converter cannot strip the XML +string element. + +The standard Jackson converter uses an isolated mapper derived from Boot's configured `JsonMapper` +for Grails domain compatibility. Persistent properties, identifiers, association identifiers, and +per-response domain projections follow Grails metadata. On this domain path, Jackson property +annotations (`@JsonIgnore`, `@JsonProperty`, `@JsonInclude`, `@JsonView`, `@JsonFormat`), property +mixins, naming strategies, and transient getters do not define the representation. Register an +explicit serializer or return a DTO when that property model is required. Ordinary non-domain beans +retain Jackson's property rules. Application-provided converter subclasses and per-type mapper +registrations keep their configured behavior and precedence. + +Association identifiers use their mapped identity property name (for example `uuid`), rather than +the legacy marshaller's fixed `id` key. A value that leads back to a domain object still being +written, such as an embedded value that points at its owner, is written as the legacy converter +writes it: `{"_ref": "..", "class": "..."}` by default, or as +`grails.converters.json.circular.reference.behaviour` selects. A cycle made only of non-domain beans +follows Jackson's rules, as it does in a plain Spring MVC controller, and fails the response. + +Boot's shared mapper is not given the Grails domain serializer, so plain Spring `@RestController` +responses retain Jackson's normal domain property model. Both mappers serialize Groovy `GString` +values as strings. Only the Grails mapper serializes Spring `Errors` as an `errors` array containing +object, field, message, and codes, without rejected values; Grails `respond errors` adapts that array into an +RFC 9457 problem response. + +Grails retains the legacy converter path when a renderer sets a legacy named converter +configuration, when a per-response `includes`/`excludes` projection is requested and the selected +converter is not Spring's standard Jackson converter, or when no configured MVC converter can write +the selected type and media type. Explicit application and plugin `Renderer` beans continue to take +precedence over the default renderer. + +Jackson conversion uses UTF-8 for intermediate bytes; the response writer applies +`grails.converters.encoding`, so non-UTF response encodings do not corrupt the intermediate JSON. + +Controller unit tests use Boot's Jackson configuration and the same setting. Supply a mapper or +`JsonMapperBuilderCustomizer` from a nested Spring `@Configuration` class, so Boot can see it before +evaluating its conditional beans. A mapper registered by the deprecated `doWithSpring` callback is too +late to make Boot back off. Named JSON +configurations work through `render` and `respond` in `ControllerUnitTest`. The optional `grails-xml` +beans are contributed by web test traits when present, without loading the XML plugin's dependency +graph. Web traits automatically include `restResponder`, even when a test overrides +`getIncludePlugins()`, and provide JSON conversion beans and Boot's mapper configuration. +They do not automatically load the controllers, URL mappings or i18n plugins; link generation retains +the curated unit-test setup. Plain `GrailsUnitTest` and `ServiceUnitTest` do not load XML or the web +plugin graph simply because that module is on the classpath. +Web unit tests build Spring MVC's message converters from the test classpath and configuration; see +<<unitTesting,Unit Testing>>. + +==== 3. JSON Converter Customization Moves to Jackson + +The `grails.converters.JSON.registerObjectMarshaller(...)` API remains supported for +`render ... as JSON` and converter-based rendering. Registering a marshaller does not change the +`respond()` rendering setting. Marshaller registrations apply to its legacy converter path, not to +Spring's message converters. To customize the Spring path, configure Spring Boot's `JsonMapper` so +the same serializer is used by opted-in `respond`, Spring MVC message conversion, and other +Jackson-based integrations: + +[source,groovy] +---- +import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer +import tools.jackson.databind.module.SimpleModule + +@Bean +JsonMapperBuilderCustomizer bookJsonCustomizer() { + { builder -> + builder.addModule(new SimpleModule() + .addSerializer(Book, new BookValueSerializer())) + } as JsonMapperBuilderCustomizer +} +---- + +`BookValueSerializer` is an application-provided Jackson `ValueSerializer<Book>`. Use a module when several +serializers belong together, or register an individual serializer directly through the builder customizer. + +For different JSON shapes selected by name, replace `JSON.createNamedConfig(...)` with the injectable +`NamedJsonConfigurationRegistry`. Each registration derives an isolated mapper from Spring Boot's configured +`JsonMapper`, so it retains application-wide Jackson settings without changing the shared mapper: + +[source,groovy] +---- +import grails.converters.json.NamedJsonConfigurationRegistry + +class BootStrap { + NamedJsonConfigurationRegistry namedJsonConfigurationRegistry + + def init = { servletContext -> + namedJsonConfigurationRegistry.register('deep') { + it.serializer(Type, new TypeValueSerializer()) + it.serializer(Category, new CategoryValueSerializer()) + } + } +} +---- + +Select the configuration per response with `respond`, or with the `json` render argument: + +[source,groovy] +---- +respond book, jsonConfiguration: 'deep' +render json: book, jsonConfiguration: 'deep' +// Without a name, use the default Jackson configuration: +render json: [ok: true] +---- + +Both accept `includes` and `excludes`. As with the legacy converter, a projection applies to the Review Comment: Reworded in eb2e6d63cc, in section 3 and the registry Javadoc: "unlike the legacy converter, which projected only a single root object". The PR description no longer promises a fallback: `jsonConfiguration:` takes only registry names. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
