Skip to content

Commit cb424a0

Browse files
committed
Start merging grace-fields into the framework
See gh-1211
2 parents e0ace00 + 454f25e commit cb424a0

File tree

50 files changed

+7833
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+7833
-0
lines changed

grace-plugin-fields/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Grace Fields Plugin
2+
3+
This plugin attempts to achieve that by using GSP templates looked up by convention.
4+
Developers can then create templates for rendering particular properties or types of properties with the former overriding the latter.

grace-plugin-fields/build.gradle

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
dependencies {
2+
api "org.graceframework:grace-web-databinding"
3+
api "org.graceframework:grace-web-gsp"
4+
api "org.graceframework:grace-plugin-validation"
5+
api "org.graceframework:grace-datastore-core:$gormVersion"
6+
api "org.graceframework:grace-datastore-gorm:$gormVersion"
7+
api "org.graceframework:grace-scaffolding-core:$scaffoldingVersion"
8+
api "org.apache.commons:commons-lang3"
9+
compileOnly "org.graceframework:grace-boot"
10+
compileOnly "jakarta.servlet:jakarta.servlet-api"
11+
12+
api "org.springframework.boot:spring-boot-autoconfigure"
13+
annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
14+
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
15+
16+
testImplementation "org.apache.groovy:groovy-dateutil"
17+
testImplementation "org.graceframework:grace-test-support"
18+
testImplementation "org.graceframework.plugins:scaffolding:$scaffoldingVersion"
19+
testImplementation "org.spockframework:spock-core", {
20+
exclude group: "org.junit.platform", module: "junit-platform-engine"
21+
}
22+
testImplementation "net.bytebuddy:byte-buddy"
23+
testImplementation "org.objenesis:objenesis:$objenesisVersion"
24+
testImplementation "org.apache.groovy:groovy-test-junit5:${groovyVersion}", {
25+
exclude group: "org.junit.platform", module: "junit-platform-launcher"
26+
exclude group: "org.junit.jupiter", module: "junit-jupiter-engine"
27+
}
28+
testImplementation "org.junit.jupiter:junit-jupiter-api"
29+
testImplementation "org.junit.platform:junit-platform-runner"
30+
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
31+
testImplementation("org.jodd:jodd-wot:$joddWotVersion") {
32+
exclude module: 'slf4j-api'
33+
exclude module: 'asm'
34+
}
35+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2012 Rob Fletcher
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+
* http://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+
17+
package grails.plugin.formfields
18+
19+
import grails.core.GrailsDomainClass
20+
import org.grails.scaffolding.model.property.Constrained
21+
import org.grails.datastore.mapping.model.PersistentEntity
22+
import org.grails.datastore.mapping.model.PersistentProperty
23+
import org.springframework.validation.FieldError
24+
25+
interface BeanPropertyAccessor {
26+
27+
/**
28+
* @return the object at the root of a path expression, e.g. for a `person` bean and `address.street` then `person` is returned.
29+
*/
30+
Object getRootBean()
31+
32+
/**
33+
* @return the type of the object at the root of a path expression, e.g. for a `person` bean and `address.street` then the type of `person` is returned.
34+
*/
35+
Class getRootBeanType()
36+
37+
/**
38+
* @return the full path from the root bean to the requested property.
39+
*/
40+
String getPathFromRoot()
41+
42+
/**
43+
* @return the name of the property at the end of the path, e.g. for `address.home.street`, `street` is returned.
44+
*/
45+
String getPropertyName()
46+
47+
/**
48+
* @return the type of the object that owns the property at the end of the path, e.g. for a `address.home.street` then the type of `home` is returned.
49+
*/
50+
Class getBeanType()
51+
52+
/**
53+
* @return the GORM domain type of `beanType`. This will be null if `beanType` is not a domain class.
54+
* @deprecated use {@link #getEntity}
55+
*/
56+
@Deprecated
57+
GrailsDomainClass getBeanClass()
58+
59+
/**
60+
* @return the GORM domain type of `beanType`. This will be null if `beanType` is not a domain class.
61+
*/
62+
PersistentEntity getEntity()
63+
64+
/**
65+
* @return all superclasses and interfaces of `beanType` excluding `Object`, `Serializable`, `Comparable` and `Cloneable`.
66+
*/
67+
List<Class> getBeanSuperclasses()
68+
69+
/**
70+
* @return the type of the property at the end of the path, e.g. for `address.home.street` then the type of `street` is returned.
71+
*/
72+
Class getPropertyType()
73+
74+
/**
75+
* @return all superclasses and interfaces of `propertyType` excluding `Object`, `Serializable`, `Comparable` and `Cloneable`.
76+
*/
77+
List<Class> getPropertyTypeSuperclasses()
78+
79+
/**
80+
* @return the value of the property at the end of the path, e.g. for `address.home.street` then the value of `street` is returned.
81+
*/
82+
Object getValue()
83+
84+
/**
85+
* @return the GORM persistent property descriptor for the property at the end of the path, e.g. for `address.home.street` then the descriptor of `street` is returned. This will be null for non-domain properties.
86+
*/
87+
PersistentProperty getDomainProperty()
88+
89+
/**
90+
* @return the constraints of the property at the end of the path, e.g. for `address.home.street` then the constraints of `street` are returned. This will be null for non-domain properties.
91+
*/
92+
Constrained getConstraints()
93+
94+
/**
95+
* @return the i18n keys used to resolve a label for the property at the end of the path in order of preference.
96+
*/
97+
List<String> getLabelKeys()
98+
99+
/**
100+
* @return default label text for the property at the end of the path.
101+
*/
102+
String getDefaultLabel()
103+
104+
/**
105+
* @return the resolved messages for any validation errors present on the property at the end of the path. This will be an empty list if there are no errors or the property is not a validateable type.
106+
*/
107+
List<FieldError> getErrors()
108+
109+
/**
110+
* @return whether or not the property is required as determined by constraints. This will always be false for non-validateable types.
111+
*/
112+
boolean isRequired()
113+
114+
/**
115+
* @return whether or not the property has any validation errors (i.e. `getErrors` will return a non-empty list). This will always be false for non-validateable types.
116+
*/
117+
boolean isInvalid()
118+
119+
}
120+
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Copyright 2012-2024 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 grails.plugin.formfields
17+
18+
import groovy.transform.CompileDynamic
19+
import groovy.transform.CompileStatic
20+
21+
import grails.core.GrailsApplication
22+
import grails.gorm.validation.DefaultConstrainedProperty
23+
import groovy.transform.PackageScope
24+
import grails.core.support.proxy.ProxyHandler
25+
import org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluator
26+
import org.grails.datastore.gorm.validation.constraints.registry.DefaultConstraintRegistry
27+
import org.grails.datastore.mapping.model.MappingContext
28+
import org.grails.datastore.mapping.model.PersistentEntity
29+
import org.grails.datastore.mapping.model.PersistentProperty
30+
import org.grails.datastore.mapping.model.types.Association
31+
import org.grails.datastore.mapping.model.types.Basic
32+
import org.grails.scaffolding.model.property.Constrained
33+
import org.grails.scaffolding.model.property.DomainProperty
34+
import org.grails.scaffolding.model.property.DomainPropertyFactory
35+
import org.springframework.beans.BeanWrapper
36+
import org.springframework.beans.BeanWrapperImpl
37+
import org.springframework.beans.PropertyAccessorFactory
38+
import org.springframework.context.support.StaticMessageSource
39+
40+
import java.lang.reflect.ParameterizedType
41+
import java.util.regex.Pattern
42+
43+
@CompileStatic
44+
class BeanPropertyAccessorFactory {
45+
46+
private GrailsApplication grailsApplication
47+
private ConstraintsEvaluator constraintsEvaluator
48+
private ProxyHandler proxyHandler
49+
private DomainPropertyFactory domainPropertyFactory
50+
private MappingContext grailsDomainClassMappingContext
51+
52+
BeanPropertyAccessorFactory(GrailsApplication grailsApplication,
53+
MappingContext grailsDomainClassMappingContext,
54+
ConstraintsEvaluator constraintsEvaluator,
55+
DomainPropertyFactory domainPropertyFactory,
56+
ProxyHandler proxyHandler) {
57+
this.grailsApplication = grailsApplication
58+
this.constraintsEvaluator = constraintsEvaluator
59+
this.proxyHandler = proxyHandler
60+
this.domainPropertyFactory = domainPropertyFactory
61+
this.grailsDomainClassMappingContext = grailsDomainClassMappingContext
62+
}
63+
64+
BeanPropertyAccessor accessorFor(bean, String propertyPath) {
65+
if (bean == null) {
66+
new PropertyPathAccessor(propertyPath)
67+
} else {
68+
resolvePropertyFromPath(bean, propertyPath)
69+
}
70+
}
71+
72+
private PersistentEntity resolveDomainClass(Class beanClass) {
73+
grailsDomainClassMappingContext.getPersistentEntity(beanClass.name)
74+
}
75+
76+
private BeanPropertyAccessor resolvePropertyFromPath(Object bean, String pathFromRoot) {
77+
def beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean)
78+
def pathElements = pathFromRoot.tokenize(".")
79+
80+
def params = [rootBean: bean, rootBeanType: bean.getClass(), pathFromRoot: pathFromRoot, grailsApplication: grailsApplication]
81+
82+
DomainProperty domainProperty = resolvePropertyFromPathComponents(beanWrapper, pathElements, params)
83+
84+
if (domainProperty != null) {
85+
new DelegatingBeanPropertyAccessorImpl(bean, params.value, params.propertyType as Class, pathFromRoot, domainProperty)
86+
} else {
87+
new BeanPropertyAccessorImpl(params)
88+
}
89+
90+
}
91+
92+
private DomainProperty resolvePropertyFromPathComponents(BeanWrapper beanWrapper, List<String> pathElements, Map<String, Object> params) {
93+
def propertyName = pathElements.remove(0)
94+
PersistentEntity beanClass = resolveDomainClass(beanWrapper.wrappedClass)
95+
def propertyType = resolvePropertyType(beanWrapper, beanClass, propertyName)
96+
def value = beanWrapper.getPropertyValue(propertyName)
97+
if (pathElements.empty) {
98+
params.value = value
99+
params.propertyType = propertyType
100+
101+
PersistentProperty persistentProperty
102+
String nameWithoutIndex = stripIndex(propertyName)
103+
if (beanClass != null) {
104+
persistentProperty = beanClass.getPropertyByName(nameWithoutIndex)
105+
if (!persistentProperty && beanClass.isIdentityName(nameWithoutIndex)) {
106+
persistentProperty = beanClass.identity
107+
}
108+
}
109+
110+
if (persistentProperty != null) {
111+
domainPropertyFactory.build(persistentProperty)
112+
} else {
113+
params.entity = beanClass
114+
params.beanType = beanWrapper.wrappedClass
115+
params.propertyType = propertyType
116+
params.propertyName = nameWithoutIndex
117+
params.domainProperty = null
118+
params.constraints = resolveConstraints(beanWrapper, (String) params.propertyName)
119+
null
120+
}
121+
} else {
122+
resolvePropertyFromPathComponents(beanWrapperFor(propertyType, value), pathElements, params)
123+
}
124+
}
125+
126+
private Constrained resolveConstraints(BeanWrapper beanWrapper, String propertyName) {
127+
grails.gorm.validation.Constrained constraint = constraintsEvaluator.evaluate(beanWrapper.wrappedClass)[propertyName]
128+
if (!constraint) {
129+
constraint = createDefaultConstraint(beanWrapper, propertyName)
130+
}
131+
new Constrained(constraint)
132+
}
133+
134+
private grails.gorm.validation.Constrained createDefaultConstraint(BeanWrapper beanWrapper, String propertyName) {
135+
def defaultConstraint = new DefaultConstrainedProperty(beanWrapper.wrappedClass, propertyName, beanWrapper.getPropertyType(propertyName), new DefaultConstraintRegistry(new StaticMessageSource()))
136+
defaultConstraint.nullable = true
137+
defaultConstraint
138+
}
139+
140+
private Class resolvePropertyType(BeanWrapper beanWrapper, PersistentEntity beanClass, String propertyName) {
141+
Class propertyType = null
142+
if (beanClass) {
143+
propertyType = resolveDomainPropertyType(beanClass, propertyName)
144+
}
145+
if (!propertyType) {
146+
propertyType = resolveNonDomainPropertyType(beanWrapper, propertyName)
147+
}
148+
propertyType
149+
}
150+
151+
private Class resolveDomainPropertyType(PersistentEntity beanClass, String propertyName) {
152+
def propertyNameWithoutIndex = stripIndex(propertyName)
153+
def persistentProperty = beanClass.getPropertyByName(propertyNameWithoutIndex)
154+
if (!persistentProperty && beanClass.isIdentityName(propertyNameWithoutIndex)) {
155+
persistentProperty = beanClass.identity
156+
}
157+
if (!persistentProperty) {
158+
return null
159+
}
160+
boolean isIndexed = propertyName =~ INDEXED_PROPERTY_PATTERN
161+
if (isIndexed) {
162+
if (persistentProperty instanceof Basic) {
163+
persistentProperty.componentType
164+
} else if (persistentProperty instanceof Association) {
165+
persistentProperty.associatedEntity.javaClass
166+
}
167+
} else {
168+
persistentProperty.type
169+
}
170+
}
171+
172+
@CompileDynamic
173+
private Class resolveNonDomainPropertyType(BeanWrapper beanWrapper, String propertyName) {
174+
def type = beanWrapper.getPropertyType(propertyName)
175+
if (type == null) {
176+
def match = propertyName =~ INDEXED_PROPERTY_PATTERN
177+
if (match) {
178+
def genericType = beanWrapper.getPropertyDescriptor(match[0][1]).readMethod.genericReturnType
179+
if (genericType instanceof ParameterizedType) {
180+
switch (genericType.rawType) {
181+
case Collection:
182+
type = genericType.actualTypeArguments[0]
183+
break
184+
case Map:
185+
type = genericType.actualTypeArguments[1]
186+
break
187+
}
188+
} else {
189+
type = Object
190+
}
191+
}
192+
}
193+
type
194+
}
195+
196+
private BeanWrapper beanWrapperFor(Class type, value) {
197+
value ? PropertyAccessorFactory.forBeanPropertyAccess(proxyHandler.unwrapIfProxy(value)) : new BeanWrapperImpl(type)
198+
}
199+
200+
private static final Pattern INDEXED_PROPERTY_PATTERN = ~/^(\w+)\[(.+)\]$/
201+
202+
@CompileDynamic
203+
@PackageScope
204+
static String stripIndex(String propertyName) {
205+
def matcher = propertyName =~ INDEXED_PROPERTY_PATTERN
206+
matcher.matches() ? matcher[0][1] : propertyName
207+
}
208+
209+
}

0 commit comments

Comments
 (0)