Skip to content

Commit 43ee981

Browse files
authored
Merge pull request #1260 from rainboyan
* pr/1260: Create grace-web-rest module * Introduce RendererRegistryCustomizer - `RendererRegistryCustomizer` allow to customize `RendererRegistry`, like `DefaultRendererRegistryCustomizer`, to add more user-defined `Renderer` Closes gh-1260
2 parents 1622cf5 + 8ee356f commit 43ee981

File tree

19 files changed

+238
-63
lines changed

19 files changed

+238
-63
lines changed

grace-plugin-rest/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ dependencies {
55
api project(':grace-plugin-api')
66
api project(":grace-plugin-controllers")
77
api project(":grace-plugin-converters")
8+
api project(":grace-web-rest")
89
api project(":grace-web-url-mappings")
910
api project(":grace-util")
1011

grace-plugin-rest/src/main/groovy/grails/artefact/controller/RestResponder.groovy

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2023 the original author or authors.
2+
* Copyright 2014-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,16 +37,15 @@ import grails.rest.render.RendererRegistry
3737
import grails.web.mime.MimeType
3838

3939
import org.grails.datastore.mapping.model.config.GormProperties
40-
import org.grails.plugins.web.rest.render.DefaultRendererRegistry
4140
import org.grails.plugins.web.rest.render.ServletRenderContext
4241
import org.grails.web.servlet.mvc.GrailsWebRequest
4342
import org.grails.web.util.GrailsApplicationAttributes
4443

4544
/**
4645
*
4746
* @author Jeff Brown
47+
* @author Michael Yan
4848
* @since 3.0
49-
*
5049
*/
5150
@CompileStatic
5251
trait RestResponder {
@@ -153,12 +152,6 @@ trait RestResponder {
153152
List<String> formats = calculateFormats(webRequest.actionName, value, args)
154153
HttpServletResponse response = webRequest.getCurrentResponse()
155154
MimeType[] mimeTypes = getResponseFormat(response)
156-
RendererRegistry registry = rendererRegistry
157-
if (registry == null) {
158-
registry = new DefaultRendererRegistry()
159-
registry.initialize()
160-
}
161-
162155
Renderer<Object> renderer = null
163156

164157
for (MimeType mimeType in mimeTypes) {
@@ -179,7 +172,7 @@ trait RestResponder {
179172
if (proxyHandler != null && target != null) {
180173
target = proxyHandler.unwrapIfProxy(target)
181174
}
182-
Renderer<Errors> errorsRenderer = registry.findContainerRenderer(mimeType, Errors, target)
175+
Renderer<Errors> errorsRenderer = rendererRegistry.findContainerRenderer(mimeType, Errors, target)
183176
if (errorsRenderer) {
184177
ServletRenderContext context = new ServletRenderContext(webRequest, [model: args.model])
185178
if (args.view) {
@@ -199,14 +192,14 @@ trait RestResponder {
199192
}
200193

201194
Class<Object> valueType = value.getClass()
202-
if (registry.isContainerType(valueType)) {
203-
renderer = registry.findContainerRenderer(mimeType, valueType, value)
195+
if (rendererRegistry.isContainerType(valueType)) {
196+
renderer = rendererRegistry.findContainerRenderer(mimeType, valueType, value)
204197
if (renderer == null) {
205-
renderer = registry.findRenderer(mimeType, value)
198+
renderer = rendererRegistry.findRenderer(mimeType, value)
206199
}
207200
}
208201
else {
209-
renderer = registry.findRenderer(mimeType, value)
202+
renderer = rendererRegistry.findRenderer(mimeType, value)
210203
}
211204
}
212205

grace-plugin-rest/src/main/groovy/org/grails/plugins/web/rest/plugin/RestResponderPluginConfiguration.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021-2023 the original author or authors.
2+
* Copyright 2021-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
1515
*/
1616
package org.grails.plugins.web.rest.plugin;
1717

18+
import java.util.List;
19+
1820
import org.springframework.beans.factory.ObjectProvider;
1921
import org.springframework.boot.autoconfigure.AutoConfiguration;
2022
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -24,8 +26,14 @@
2426
import grails.config.Config;
2527
import grails.config.Settings;
2628
import grails.core.GrailsApplication;
29+
import grails.core.support.proxy.ProxyHandler;
30+
import grails.rest.render.Renderer;
31+
import grails.rest.render.RendererRegistry;
32+
import grails.rest.render.RendererRegistryCustomizer;
2733

2834
import org.grails.plugins.web.rest.render.DefaultRendererRegistry;
35+
import org.grails.plugins.web.rest.render.DefaultRendererRegistryCustomizer;
36+
import org.grails.web.gsp.io.GrailsConventionGroovyPageLocator;
2937

3038
/**
3139
* {@link EnableAutoConfiguration Auto-configuration} for Grails RestResponder Plugin.
@@ -38,14 +46,51 @@
3846
public class RestResponderPluginConfiguration {
3947

4048
@Bean
41-
public DefaultRendererRegistry rendererRegistry(ObjectProvider<GrailsApplication> grailsApplication) {
49+
public RendererRegistry rendererRegistry(ObjectProvider<GrailsApplication> grailsApplication,
50+
ObjectProvider<GrailsConventionGroovyPageLocator> groovyPageLocatorObjectProvider,
51+
ObjectProvider<ProxyHandler> proxyHandlerObjectProvider,
52+
ObjectProvider<Renderer<?>> rendererObjectProvider,
53+
ObjectProvider<RendererRegistryCustomizer> rendererRegistryCustomizers) {
4254
Config config = grailsApplication.getIfAvailable().getConfig();
4355
String modelSuffix = config.getProperty(Settings.SCAFFOLDING_DOMAIN_SUFFIX, "");
4456

57+
GrailsConventionGroovyPageLocator groovyPageLocator = groovyPageLocatorObjectProvider.getIfAvailable();
58+
ProxyHandler proxyHandler = proxyHandlerObjectProvider.getIfAvailable();
59+
4560
DefaultRendererRegistry rendererRegistry = new DefaultRendererRegistry();
4661
rendererRegistry.setModelSuffix(modelSuffix);
62+
rendererRegistry.setGroovyPageLocator(groovyPageLocator);
63+
rendererRegistry.setProxyHandler(proxyHandler);
64+
65+
List<Renderer<?>> renderers = rendererObjectProvider.stream().toList();
66+
rendererRegistry.setRenderers(renderers.toArray(Renderer[]::new));
67+
68+
applyCustomizers(rendererRegistry, rendererRegistryCustomizers);
4769

4870
return rendererRegistry;
4971
}
5072

73+
private void applyCustomizers(DefaultRendererRegistry rendererRegistry,
74+
ObjectProvider<RendererRegistryCustomizer> rendererRegistryCustomizers) {
75+
Iterable<RendererRegistryCustomizer> orderedCustomizers = () -> rendererRegistryCustomizers.orderedStream()
76+
.iterator();
77+
for (RendererRegistryCustomizer customizer : orderedCustomizers) {
78+
customizer.customize(rendererRegistry);
79+
}
80+
}
81+
82+
@Bean
83+
public RendererRegistryCustomizer defaultRendererRegistryCustomizer(ObjectProvider<GrailsApplication> grailsApplication,
84+
ObjectProvider<GrailsConventionGroovyPageLocator> groovyPageLocatorObjectProvider,
85+
ObjectProvider<ProxyHandler> proxyHandlerObjectProvider) {
86+
Config config = grailsApplication.getIfAvailable().getConfig();
87+
String modelSuffix = config.getProperty(Settings.SCAFFOLDING_DOMAIN_SUFFIX, "");
88+
89+
DefaultRendererRegistryCustomizer rendererRegistryCustomizer = new DefaultRendererRegistryCustomizer();
90+
rendererRegistryCustomizer.setGroovyPageLocator(groovyPageLocatorObjectProvider.getIfAvailable());
91+
rendererRegistryCustomizer.setProxyHandler(proxyHandlerObjectProvider.getIfAvailable());
92+
rendererRegistryCustomizer.setModelSuffix(modelSuffix);
93+
return rendererRegistryCustomizer;
94+
}
95+
5196
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2022-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.grails.plugins.web.rest.render;
17+
18+
import org.springframework.validation.Errors;
19+
20+
import grails.core.support.proxy.ProxyHandler;
21+
import grails.rest.render.RendererRegistry;
22+
import grails.rest.render.RendererRegistryCustomizer;
23+
import grails.web.mime.MimeType;
24+
import org.grails.plugins.web.rest.render.html.DefaultHtmlRenderer;
25+
import org.grails.plugins.web.rest.render.json.DefaultJsonRenderer;
26+
import org.grails.plugins.web.rest.render.xml.DefaultXmlRenderer;
27+
import org.grails.web.gsp.io.GrailsConventionGroovyPageLocator;
28+
29+
/**
30+
* Default {@link RendererRegistryCustomizer} to initialize RendererRegistry.
31+
*
32+
* @author Michael Yan
33+
* @since 2024.0.0
34+
*/
35+
public class DefaultRendererRegistryCustomizer implements RendererRegistryCustomizer {
36+
37+
private GrailsConventionGroovyPageLocator groovyPageLocator;
38+
private ProxyHandler proxyHandler;
39+
private String modelSuffix;
40+
41+
public DefaultRendererRegistryCustomizer() {
42+
}
43+
44+
public void setGroovyPageLocator(GrailsConventionGroovyPageLocator groovyPageLocator) {
45+
this.groovyPageLocator = groovyPageLocator;
46+
}
47+
48+
public void setProxyHandler(ProxyHandler proxyHandler) {
49+
this.proxyHandler = proxyHandler;
50+
}
51+
52+
public void setModelSuffix(String modelSuffix) {
53+
this.modelSuffix = modelSuffix;
54+
}
55+
56+
@Override
57+
public void customize(RendererRegistry rendererRegistry) {
58+
DefaultXmlRenderer<Object> defaultXmlRenderer = new DefaultXmlRenderer<>(Object.class);
59+
defaultXmlRenderer.setGroovyPageLocator(groovyPageLocator);
60+
defaultXmlRenderer.setRendererRegistry(rendererRegistry);
61+
rendererRegistry.addDefaultRenderer(defaultXmlRenderer);
62+
63+
DefaultJsonRenderer<Object> defaultJsonRenderer = new DefaultJsonRenderer<>(Object.class);
64+
defaultJsonRenderer.setGroovyPageLocator(groovyPageLocator);
65+
defaultJsonRenderer.setRendererRegistry(rendererRegistry);
66+
rendererRegistry.addDefaultRenderer(defaultJsonRenderer);
67+
68+
DefaultHtmlRenderer<Object> defaultHtmlRenderer = new DefaultHtmlRenderer<>(Object.class);
69+
defaultHtmlRenderer.setSuffix(modelSuffix);
70+
defaultHtmlRenderer.setProxyHandler(proxyHandler);
71+
rendererRegistry.addDefaultRenderer(defaultHtmlRenderer);
72+
73+
DefaultHtmlRenderer<Object> allHtmlRenderer = new DefaultHtmlRenderer<>(Object.class, MimeType.ALL);
74+
allHtmlRenderer.setSuffix(modelSuffix);
75+
allHtmlRenderer.setProxyHandler(proxyHandler);
76+
rendererRegistry.addDefaultRenderer(allHtmlRenderer);
77+
78+
DefaultXmlRenderer<Errors> defaultContainerXmlRenderer = new DefaultXmlRenderer<>(Errors.class, MimeType.XML, MimeType.TEXT_XML);
79+
defaultContainerXmlRenderer.setGroovyPageLocator(groovyPageLocator);
80+
defaultContainerXmlRenderer.setRendererRegistry(rendererRegistry);
81+
rendererRegistry.addContainerRenderer(Object.class, defaultContainerXmlRenderer);
82+
83+
DefaultJsonRenderer<Errors> defaultContainerJsonRenderer = new DefaultJsonRenderer<>(Errors.class, MimeType.JSON, MimeType.TEXT_JSON);
84+
defaultContainerJsonRenderer.setGroovyPageLocator(groovyPageLocator);
85+
defaultContainerJsonRenderer.setRendererRegistry(rendererRegistry);
86+
rendererRegistry.addContainerRenderer(Object.class, defaultContainerJsonRenderer);
87+
88+
DefaultHtmlRenderer<Errors> defaultContainerHtmlRenderer = new DefaultHtmlRenderer<>(Errors.class);
89+
defaultContainerHtmlRenderer.setSuffix(modelSuffix);
90+
defaultContainerHtmlRenderer.setProxyHandler(proxyHandler);
91+
rendererRegistry.addContainerRenderer(Object.class, defaultContainerHtmlRenderer);
92+
93+
DefaultHtmlRenderer<Errors> defaultContainerAllRenderer = new DefaultHtmlRenderer<>(Errors.class, MimeType.ALL);
94+
defaultContainerAllRenderer.setSuffix(modelSuffix);
95+
defaultContainerAllRenderer.setProxyHandler(proxyHandler);
96+
rendererRegistry.addContainerRenderer(Object.class, defaultContainerAllRenderer);
97+
}
98+
99+
}

grace-plugin-rest/src/test/groovy/org/grails/plugins/web/rest/render/DefaultRendererRegistrySpec.groovy

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package org.grails.plugins.web.rest.render
1817

1918
import grails.rest.render.AbstractRenderer
@@ -24,6 +23,8 @@ import org.springframework.validation.BeanPropertyBindingResult
2423
import org.springframework.validation.Errors
2524
import spock.lang.Specification
2625

26+
import org.grails.plugins.web.rest.render.xml.DefaultXmlRenderer
27+
2728
class DefaultRendererRegistrySpec extends Specification {
2829

2930
void "Test that registering a HAL collection renderer works"() {
@@ -42,8 +43,7 @@ class DefaultRendererRegistrySpec extends Specification {
4243
void "Test that the registry returns an appropriate render for a container type"() {
4344
when:"A registry with a specific renderer"
4445
def registry = new DefaultRendererRegistry()
45-
registry.initialize()
46-
46+
registry.addContainerRenderer(Object, new DefaultXmlRenderer<>(Errors))
4747

4848
then:"An errors renderer can be found"
4949
registry.findContainerRenderer(MimeType.XML, Errors, new BeanPropertyBindingResult("foo", "bar"))
@@ -66,8 +66,8 @@ class DefaultRendererRegistrySpec extends Specification {
6666
void "Test that registry returns appropriate renderer for type"() {
6767
given:"A registry with a specific renderer"
6868
def registry = new DefaultRendererRegistry()
69-
registry.initialize()
7069
def mimeType = new MimeType("text/xml", 'xml')
70+
registry.addDefaultRenderer(new DefaultXmlRenderer(Object, mimeType))
7171
registry.addRenderer(new AbstractRenderer(URL,mimeType) {
7272
@Override
7373
void render(Object object, RenderContext context) {
@@ -85,8 +85,8 @@ class DefaultRendererRegistrySpec extends Specification {
8585
void "Test that registry returns appropriate renderer for subclass"() {
8686
given:"A registry with a specific renderer"
8787
def registry = new DefaultRendererRegistry()
88-
registry.initialize()
8988
def mimeType = new MimeType("text/xml", 'xml')
89+
registry.addDefaultRenderer(new DefaultXmlRenderer(Object, mimeType))
9090
registry.addRenderer(new AbstractRenderer(CharSequence,mimeType) {
9191
@Override
9292
void render(Object object, RenderContext context) {
@@ -105,8 +105,8 @@ class DefaultRendererRegistrySpec extends Specification {
105105
void "Test that registry fallbacks to a default renderer if none found"() {
106106
given:"A registry with a specific renderer"
107107
def registry = new DefaultRendererRegistry()
108-
registry.initialize()
109108
def mimeType = new MimeType("text/xml", 'xml')
109+
registry.addDefaultRenderer(new DefaultXmlRenderer(Object, mimeType))
110110
registry.addDefaultRenderer(new AbstractRenderer(Object,mimeType) {
111111
@Override
112112
void render(Object object, RenderContext context) {
@@ -120,5 +120,5 @@ class DefaultRendererRegistrySpec extends Specification {
120120
registry.findRenderer(mimeType, "foo")
121121
registry.findRenderer(mimeType, "foo").mimeTypes.contains mimeType
122122
}
123-
}
124123

124+
}

grace-test-support/src/main/groovy/org/grails/testing/spock/WebSetupSpecInterceptor.groovy

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import grails.artefact.ArtefactTypes
2727
import grails.config.Config
2828
import grails.config.Settings
2929
import grails.core.GrailsApplication
30+
import grails.rest.render.RendererRegistry
31+
import grails.rest.render.RendererRegistryCustomizer
3032
import grails.testing.web.GrailsWebUnitTest
3133
import grails.testing.web.controllers.ControllerUnitTest
3234
import grails.web.CamelCaseUrlConverter
@@ -40,6 +42,7 @@ import org.grails.gsp.jsp.TagLibraryResolverImpl
4042
import org.grails.plugins.codecs.CodecsGrailsPlugin
4143
import org.grails.plugins.codecs.DefaultCodecLookup
4244
import org.grails.plugins.web.rest.render.DefaultRendererRegistry
45+
import org.grails.plugins.web.rest.render.DefaultRendererRegistryCustomizer
4346
import org.grails.testing.runtime.support.GroovyPageUnitTestResourceLoader
4447
import org.grails.testing.runtime.support.LazyTagLibraryLookup
4548
import org.grails.validation.ConstraintEvalUtils
@@ -90,6 +93,13 @@ class WebSetupSpecInterceptor implements IMethodInterceptor {
9093

9194
rendererRegistry(DefaultRendererRegistry) {
9295
modelSuffix = config.getProperty('grails.scaffolding.templates.domainSuffix', '')
96+
groovyPageLocator = ref('groovyPageLocator')
97+
proxyHandler = ref('proxyHandler')
98+
}
99+
defaultRendererRegistryCustomizer(DefaultRendererRegistryCustomizer) {
100+
modelSuffix = config.getProperty('grails.scaffolding.templates.domainSuffix', '')
101+
groovyPageLocator = ref('groovyPageLocator')
102+
proxyHandler = ref('proxyHandler')
93103
}
94104
String urlConverterType = config.getProperty(Settings.WEB_URL_CONVERTER)
95105
"${grails.web.UrlConverter.BEAN_NAME}"(urlConverterType == 'hyphenated' ? HyphenatedUrlConverter : CamelCaseUrlConverter)
@@ -167,6 +177,9 @@ class WebSetupSpecInterceptor implements IMethodInterceptor {
167177
}
168178

169179
grailsApplication.mainContext.getBean(DefaultCodecLookup).reInitialize()
180+
Collection<RendererRegistryCustomizer> rendererRegistryCustomizers = grailsApplication.mainContext.getBeansOfType(RendererRegistryCustomizer).values()
181+
RendererRegistry rendererRegistry = grailsApplication.mainContext.getBean(RendererRegistry)
182+
rendererRegistryCustomizers.each { it.customize(rendererRegistry) }
170183
}
171184

172185
}

grace-web-rest/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
## grace-web-rest

grace-web-rest/build.gradle

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
dependencies {
2+
api project(":grace-web")
3+
api project(":grace-web-gsp")
4+
api project(":grace-web-mvc")
5+
api project(":grace-web-url-mappings")
6+
7+
compileOnlyApi libs.jakarta.servlet
8+
compileOnly(libs.grace.datastore.gorm) {
9+
transitive = false
10+
}
11+
compileOnly libs.grace.datastore.gorm.support
12+
implementation libs.caffeine
13+
14+
testImplementation libs.jakarta.servlet
15+
testImplementation libs.spring.test
16+
}

grace-plugin-rest/src/main/groovy/grails/rest/render/AbstractIncludeExcludeRenderer.groovy renamed to grace-web-rest/src/main/groovy/grails/rest/render/AbstractIncludeExcludeRenderer.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2022 the original author or authors.
2+
* Copyright 2013-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)