findInputProperties(PersistentEntity domainClass, Closure closure)
+
+ /**
+ * Determines if the closure returns true for any input {@link DomainProperty}
+ *
+ * @param domainClass The persistent entity
+ * @param closure The closure that will be executed for each property
+ */
+ Boolean hasInputProperty(PersistentEntity domainClass, Closure closure)
+
+}
diff --git a/src/main/groovy/org/grails/scaffolding/model/DomainModelServiceImpl.groovy b/src/main/groovy/org/grails/scaffolding/model/DomainModelServiceImpl.groovy
new file mode 100644
index 00000000..4feb6438
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/model/DomainModelServiceImpl.groovy
@@ -0,0 +1,162 @@
+package org.grails.scaffolding.model
+
+import org.grails.datastore.mapping.config.Property
+import org.grails.scaffolding.model.property.Constrained
+import org.grails.scaffolding.model.property.DomainProperty
+import org.grails.scaffolding.model.property.DomainPropertyFactory
+import grails.util.GrailsClassUtils
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.model.types.Embedded
+import org.springframework.beans.factory.annotation.Autowired
+
+import java.lang.reflect.Method
+
+/**
+ * @see {@link DomainModelService}
+ * @author James Kleeh
+ */
+@CompileStatic
+class DomainModelServiceImpl implements DomainModelService {
+
+ @Autowired
+ DomainPropertyFactory domainPropertyFactory
+
+ private static Method derivedMethod
+
+ static {
+ try {
+ derivedMethod = Property.class.getMethod("isDerived", (Class>[]) null)
+ } catch (NoSuchMethodException | SecurityException e) {
+ // no-op
+ }
+ }
+
+ /**
+ * Retrieves persistent properties and excludes:
+ * - Any properties listed in the {@code static scaffold = [exclude: []]} property on the domain class
+ *
- Any properties that have the constraint {@code [display: false]}
+ *
- Any properties whose name exist in the blackList
+ *
+ *
+ * @see {@link DomainModelService#getInputProperties}
+ * @param domainClass The persistent entity
+ * @param blackList The list of domain class property names to exclude
+ */
+ protected List getProperties(PersistentEntity domainClass, List blacklist) {
+ List properties = domainClass.persistentProperties.collect {
+ domainPropertyFactory.build(it)
+ }
+ Object scaffoldProp = GrailsClassUtils.getStaticPropertyValue(domainClass.javaClass, 'scaffold')
+ if (scaffoldProp instanceof Map) {
+ Map scaffold = (Map)scaffoldProp
+ if (scaffold.containsKey('exclude')) {
+ if (scaffold.exclude instanceof Collection) {
+ blacklist.addAll((Collection)scaffold.exclude)
+ } else if (scaffold.exclude instanceof String) {
+ blacklist.add((String)scaffold.exclude)
+ }
+ }
+ }
+
+ properties.removeAll {
+ if (it.name in blacklist) {
+ return true
+ }
+ Constrained constrained = it.constrained
+ if (constrained && !constrained.display) {
+ return true
+ }
+ if (derivedMethod != null) {
+ Property property = it.mapping.mappedForm
+ if (derivedMethod.invoke(property, (Object[]) null)) {
+ return true
+ }
+ }
+
+ false
+ }
+ properties.sort()
+ properties
+ }
+
+ /**
+ * Blacklist:
+ * - version
+ *
- dateCreated
+ *
- lastUpdated
+ *
+ *
+ * @see {@link DomainModelServiceImpl#getProperties}
+ * @param domainClass The persistent entity
+ */
+ List getInputProperties(PersistentEntity domainClass) {
+ getProperties(domainClass, ['version', 'dateCreated', 'lastUpdated'])
+ }
+
+ /**
+ * Blacklist:
+ *
+ * @see {@link DomainModelServiceImpl#getProperties}
+ * @param domainClass The persistent entity
+ */
+ List getOutputProperties(PersistentEntity domainClass) {
+ getProperties(domainClass, ['version'])
+ }
+
+ /**
+ * The same as {@link #getOutputProperties(org.grails.datastore.mapping.model.PersistentEntity)} except the identifier is prepended
+ *
+ * @see {@link DomainModelServiceImpl#getOutputProperties}
+ * @param domainClass The persistent entity
+ */
+ List getListOutputProperties(PersistentEntity domainClass) {
+ List properties = getOutputProperties(domainClass)
+ properties.add(0, domainPropertyFactory.build(domainClass.identity))
+ properties
+ }
+
+ /**
+ * Will return all properties in a domain class that the provided closure returns
+ * true for. Searches embedded properties
+ *
+ * @see {@link DomainModelService#findInputProperties}
+ * @param domainClass The persistent entity
+ * @param closure The closure that will be executed for each editable property
+ */
+ List findInputProperties(PersistentEntity domainClass, Closure closure) {
+ List properties = []
+ getInputProperties(domainClass).each { DomainProperty domainProperty ->
+ PersistentProperty property = domainProperty.persistentProperty
+ if (property instanceof Embedded) {
+ getInputProperties(((Embedded)property).associatedEntity).each { DomainProperty embedded ->
+ embedded.rootProperty = domainProperty
+ if (closure.call(embedded)) {
+ properties.add(embedded)
+ }
+ }
+ } else {
+ if (closure.call(domainProperty)) {
+ properties.add(domainProperty)
+ }
+ }
+ }
+ properties
+ }
+
+ /**
+ * Returns true if the provided closure returns true for any domain class
+ * property. Searches embedded properties
+ *
+ * @see {@link DomainModelService#hasInputProperty}
+ * @param domainClass The persistent entity
+ * @param closure The closure that will be executed for each editable property
+ */
+ Boolean hasInputProperty(PersistentEntity domainClass, Closure closure) {
+ findInputProperties(domainClass, closure).size() > 0
+ }
+
+}
diff --git a/src/main/groovy/org/grails/scaffolding/model/property/Constrained.groovy b/src/main/groovy/org/grails/scaffolding/model/property/Constrained.groovy
new file mode 100644
index 00000000..8512f388
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/model/property/Constrained.groovy
@@ -0,0 +1,215 @@
+package org.grails.scaffolding.model.property
+
+
+class Constrained {
+
+ grails.gorm.validation.Constrained constrained1
+
+ Constrained(grails.gorm.validation.Constrained constrained1) {
+ this.constrained1 = constrained1
+ }
+
+ Object callMethod(String name, Object arguments) {
+ if (this.constrained1 != null) {
+ this.constrained1.invokeMethod(name, arguments)
+ } else {
+ null
+ }
+ }
+
+ boolean hasAppliedConstraint(String name) {
+ if (this.constrained1 != null) {
+ this.constrained1.hasAppliedConstraint(name)
+ } else {
+ false
+ }
+ }
+
+ int getOrder() {
+ if (this.constrained1 != null) {
+ this.constrained1.order
+ } else {
+ 0
+ }
+ }
+
+ boolean isNullable() {
+ if (this.constrained1 != null) {
+ this.constrained1.nullable
+ } else {
+ false
+ }
+ }
+
+ boolean isBlank() {
+ if (this.constrained1 != null) {
+ this.constrained1.blank
+ } else {
+ false
+ }
+ }
+
+ boolean isDisplay() {
+ if (this.constrained1 != null) {
+ this.constrained1.display
+ } else {
+ true
+ }
+ }
+
+ boolean isEditable() {
+ if (this.constrained1 != null) {
+ this.constrained1.editable
+ } else {
+ true
+ }
+ }
+
+ List getInList() {
+ if (this.constrained1 != null) {
+ this.constrained1.inList
+ } else {
+ null
+ }
+ }
+
+ Range getRange() {
+ if (this.constrained1 != null) {
+ this.constrained1.range
+ } else {
+ null
+ }
+ }
+
+ Integer getScale() {
+ if (this.constrained1 != null) {
+ this.constrained1.scale
+ } else {
+ null
+ }
+ }
+
+ Comparable getMin() {
+ if (this.constrained1 != null) {
+ this.constrained1.min
+ } else {
+ null
+ }
+ }
+
+ Comparable getMax() {
+ if (this.constrained1 != null) {
+ this.constrained1.max
+ } else {
+ null
+ }
+ }
+
+ Range getSize() {
+ if (this.constrained1 != null) {
+ this.constrained1.size
+ } else {
+ null
+ }
+ }
+
+ Integer getMaxSize() {
+ if (this.constrained1 != null) {
+ this.constrained1.maxSize
+ } else {
+ null
+ }
+ }
+
+ String getWidget() {
+ if (this.constrained1 != null) {
+ this.constrained1.widget
+ } else {
+ null
+ }
+ }
+
+ boolean isPassword() {
+ if (this.constrained1 != null) {
+ this.constrained1.password
+ } else {
+ false
+ }
+ }
+
+ boolean isEmail() {
+ if (this.constrained1 != null) {
+ this.constrained1.email
+ } else {
+ false
+ }
+ }
+
+ boolean isCreditCard() {
+ if (this.constrained1 != null) {
+ this.constrained1.creditCard
+ } else {
+ false
+ }
+ }
+
+ boolean isUrl() {
+ if (this.constrained1 != null) {
+ this.constrained1.url
+ } else {
+ false
+ }
+ }
+
+ String getMatches() {
+ if (this.constrained1 != null) {
+ this.constrained1.matches
+ } else {
+ null
+ }
+ }
+
+ Object getNotEqual() {
+ if (this.constrained1 != null) {
+ this.constrained1.notEqual
+ } else {
+ null
+ }
+ }
+
+ Integer getMinSize() {
+ if (this.constrained1 != null) {
+ this.constrained1.minSize
+ } else {
+ null
+ }
+ }
+
+ String getFormat() {
+ if (this.constrained1 != null) {
+ this.constrained1.format
+ } else {
+ null
+ }
+ }
+
+ void applyConstraint(String constraintName, Object constrainingValue) {
+ if (this.constrained1 != null) {
+ this.constrained1.applyConstraint(constraintName, constrainingValue)
+ } else {
+ null
+ }
+ }
+
+ Class getOwner() {
+ if (this.constrained1 != null) {
+ this.constrained1.owner
+ } else {
+ null
+ }
+ }
+
+ boolean isNull() {
+ this.constrained1 == null
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/model/property/DomainProperty.groovy b/src/main/groovy/org/grails/scaffolding/model/property/DomainProperty.groovy
new file mode 100644
index 00000000..8cca6e48
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/model/property/DomainProperty.groovy
@@ -0,0 +1,81 @@
+package org.grails.scaffolding.model.property
+
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+
+/**
+ * An API to join the {@link PersistentProperty} to the {@link org.springframework.validation.Validator}
+ * to assist with scaffolding
+ *
+ * @author James Kleeh
+ */
+interface DomainProperty extends PersistentProperty, Comparable {
+
+ /**
+ * @return The path of the property from the root domain class
+ */
+ String getPathFromRoot()
+
+ /**
+ * @return The {@link PersistentProperty} that represents this property
+ */
+ PersistentProperty getPersistentProperty()
+
+ /**
+ * @return The {@link PersistentEntity} the property belongs to
+ */
+ PersistentEntity getDomainClass()
+
+ /**
+ * @return The constraints of the property
+ */
+ Constrained getConstrained()
+
+ /**
+ * @return The root property
+ */
+ PersistentProperty getRootProperty()
+
+ /**
+ * Sets the root property
+ *
+ * @param rootProperty The root property
+ */
+ void setRootProperty(PersistentProperty rootProperty)
+
+ /**
+ * @return The class the root property belongs to
+ */
+ Class getRootBeanType()
+
+ /**
+ * @return The class the property belongs to
+ */
+ Class getBeanType()
+
+ /**
+ * @return The type of the association
+ */
+ Class getAssociatedType()
+
+ /**
+ * @return The associated entity if the property is an assocation
+ */
+ PersistentEntity getAssociatedEntity()
+
+ /**
+ * @return Whether or not the property is required
+ */
+ boolean isRequired()
+
+ /**
+ * @return i18n message keys to resolve the label of the property
+ */
+ List getLabelKeys()
+
+ /**
+ * @return The default label for the property (natural name)
+ */
+ String getDefaultLabel()
+
+}
\ No newline at end of file
diff --git a/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactory.groovy b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactory.groovy
new file mode 100644
index 00000000..a2af4577
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactory.groovy
@@ -0,0 +1,25 @@
+package org.grails.scaffolding.model.property
+
+import org.grails.datastore.mapping.model.PersistentProperty
+
+/**
+ * A factory to create instances of {@link DomainProperty}
+ *
+ * @author James Kleeh
+ */
+interface DomainPropertyFactory {
+
+ /**
+ * @param persistentProperty The persistent property
+ * @return The {@link DomainProperty} representing the {@link PersistentProperty}
+ */
+ DomainProperty build(PersistentProperty persistentProperty)
+
+ /**
+ * @param rootProperty The root property. Typically an instance of {@link org.grails.datastore.mapping.model.types.Embedded}
+ * @param persistentProperty The persistent property
+ * @return The {@link DomainProperty} representing the {@link PersistentProperty}
+ */
+ DomainProperty build(PersistentProperty rootProperty, PersistentProperty persistentProperty)
+
+}
diff --git a/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactoryImpl.groovy b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactoryImpl.groovy
new file mode 100644
index 00000000..4464ed9e
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyFactoryImpl.groovy
@@ -0,0 +1,41 @@
+package org.grails.scaffolding.model.property
+
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+
+/**
+ * @see {@link DomainPropertyFactory}
+ * @author James Kleeh
+ */
+@CompileStatic
+class DomainPropertyFactoryImpl implements DomainPropertyFactory {
+
+ @Value('${grails.databinding.convertEmptyStringsToNull:true}')
+ Boolean convertEmptyStringsToNull
+
+ @Value('${grails.databinding.trimStrings:true}')
+ Boolean trimStrings
+
+ @Autowired
+ MappingContext grailsDomainClassMappingContext
+
+ DomainProperty build(PersistentProperty persistentProperty) {
+ DomainPropertyImpl domainProperty = new DomainPropertyImpl(persistentProperty, grailsDomainClassMappingContext)
+ init(domainProperty)
+ domainProperty
+ }
+
+ DomainProperty build(PersistentProperty rootProperty, PersistentProperty persistentProperty) {
+ DomainPropertyImpl domainProperty = new DomainPropertyImpl(rootProperty, persistentProperty, grailsDomainClassMappingContext)
+ init(domainProperty)
+ domainProperty
+ }
+
+ private init(DomainPropertyImpl domainProperty) {
+ domainProperty.convertEmptyStringsToNull = convertEmptyStringsToNull
+ domainProperty.trimStrings = trimStrings
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyImpl.groovy b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyImpl.groovy
new file mode 100644
index 00000000..b2742803
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/model/property/DomainPropertyImpl.groovy
@@ -0,0 +1,139 @@
+package org.grails.scaffolding.model.property
+
+import grails.gorm.validation.PersistentEntityValidator
+import grails.util.GrailsNameUtils
+import groovy.transform.CompileStatic
+import org.grails.datastore.mapping.config.Property
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.model.types.Association
+import org.grails.datastore.mapping.model.types.Basic
+import org.springframework.validation.Validator
+import static grails.gorm.validation.ConstrainedProperty.BLANK_CONSTRAINT
+
+/**
+ * @see {@link DomainProperty}
+ * @author James Kleeh
+ */
+@CompileStatic
+class DomainPropertyImpl implements DomainProperty {
+
+ @Delegate PersistentProperty persistentProperty
+ PersistentProperty rootProperty
+ PersistentEntity domainClass
+ Constrained constrained
+ String pathFromRoot
+
+ protected Boolean convertEmptyStringsToNull
+ protected Boolean trimStrings
+
+ DomainPropertyImpl(PersistentProperty persistentProperty, MappingContext mappingContext) {
+ this.persistentProperty = persistentProperty
+ this.domainClass = persistentProperty.owner
+ Validator validator = mappingContext.getEntityValidator(domainClass)
+ if (validator instanceof PersistentEntityValidator) {
+ this.constrained = new Constrained(((PersistentEntityValidator)validator).constrainedProperties.get(name))
+ }
+ if (this.constrained?.isNull()) {
+ this.constrained = null
+ }
+
+ this.pathFromRoot = persistentProperty.name
+ }
+
+ DomainPropertyImpl(PersistentProperty rootProperty, PersistentProperty persistentProperty, MappingContext mappingContext) {
+ this(persistentProperty, mappingContext)
+ this.setRootProperty(rootProperty)
+ }
+
+ void setRootProperty(PersistentProperty rootProperty) {
+ this.rootProperty = rootProperty
+ this.pathFromRoot = "${rootProperty.name}.${name}"
+ }
+
+ Class getRootBeanType() {
+ (rootProperty ?: persistentProperty).owner.javaClass
+ }
+
+ Class getBeanType() {
+ owner.javaClass
+ }
+
+ Class getAssociatedType() {
+ if (persistentProperty instanceof Association) {
+ if (persistentProperty instanceof Basic) {
+ ((Basic)persistentProperty).componentType
+ } else {
+ associatedEntity.javaClass
+ }
+ } else {
+ null
+ }
+ }
+
+ PersistentEntity getAssociatedEntity() {
+ ((Association)persistentProperty).associatedEntity
+ }
+
+ boolean isRequired() {
+ if (type in [Boolean, boolean]) {
+ false
+ } else if (type == String) {
+ // if the property prohibits nulls and blanks are converted to nulls, then blanks will be prohibited even if a blank
+ // constraint does not exist
+ boolean hasBlankConstraint = constrained?.hasAppliedConstraint(BLANK_CONSTRAINT)
+ boolean blanksImplicityProhibited = !hasBlankConstraint && !constrained?.nullable && convertEmptyStringsToNull && trimStrings
+ !constrained?.nullable && (!constrained?.blank || blanksImplicityProhibited)
+ } else {
+ !constrained?.nullable
+ }
+ }
+
+ List getLabelKeys() {
+ List labelKeys = []
+ labelKeys.add("${GrailsNameUtils.getPropertyName(beanType.simpleName)}.${name}.label".toString())
+ if (rootProperty) {
+ labelKeys.add("${GrailsNameUtils.getPropertyName(rootBeanType.simpleName)}.${pathFromRoot}.label".replaceAll(/\[(.+)\]/, '').toString())
+ }
+ labelKeys.unique()
+ }
+
+ String getDefaultLabel() {
+ GrailsNameUtils.getNaturalName(name)
+ }
+
+ int compareTo(DomainProperty o2) {
+
+ if (domainClass.mapping.identifier?.identifierName?.contains(name)) {
+ return -1
+ }
+ if (domainClass.mapping.identifier?.identifierName?.contains(o2.name)) {
+ return 1
+ }
+
+ Constrained cp2 = o2.constrained
+
+ if (constrained == null && cp2 == null) {
+ return name.compareTo(o2.name)
+ }
+
+ if (constrained == null) {
+ return 1
+ }
+
+ if (cp2 == null) {
+ return -1
+ }
+
+ if (constrained.order > cp2.order) {
+ return 1
+ }
+
+ if (constrained.order < cp2.order) {
+ return -1
+ }
+
+ return 0
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainInputRenderer.groovy
new file mode 100644
index 00000000..c896741b
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/DomainInputRenderer.groovy
@@ -0,0 +1,21 @@
+package org.grails.scaffolding.registry
+
+import org.grails.scaffolding.model.property.DomainProperty
+
+/**
+ * Used to render a single domain class property on a form
+ *
+ * @author James Kleeh
+ */
+interface DomainInputRenderer extends DomainRenderer {
+
+ /**
+ * Defines how a given domain class property will be rendered in the context of a form
+ *
+ * @param defaultAttributes The default html element attributes
+ * @param property The domain property to be rendered
+ * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder}
+ */
+ Closure renderInput(Map defaultAttributes, DomainProperty property)
+
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainInputRendererRegistry.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainInputRendererRegistry.groovy
new file mode 100644
index 00000000..68a7b6a0
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/DomainInputRendererRegistry.groovy
@@ -0,0 +1,13 @@
+package org.grails.scaffolding.registry
+
+import groovy.transform.CompileStatic
+
+/**
+ * A registry of {@link DomainInputRenderer} instances
+ *
+ * @author James Kleeh
+ */
+@CompileStatic
+class DomainInputRendererRegistry extends DomainRendererRegistry {
+
+}
\ No newline at end of file
diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRenderer.groovy
new file mode 100644
index 00000000..db2ce0e2
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRenderer.groovy
@@ -0,0 +1,27 @@
+package org.grails.scaffolding.registry
+
+import org.grails.scaffolding.model.property.DomainProperty
+
+/**
+ * Used to render markup that represents how single domain class property will be displayed
+ *
+ * @author James Kleeh
+ */
+interface DomainOutputRenderer extends DomainRenderer {
+
+ /**
+ * Defines how a given domain class property will be rendered in the context of a list of domains class instances
+ *
+ * @param property The domain property to be rendered
+ * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder}
+ */
+ Closure renderListOutput(DomainProperty property)
+
+ /**
+ * Defines how a given domain class property will be rendered in the context of a single domain class instance
+ *
+ * @param property The domain property to be rendered
+ * @return The closure to be passed to an instance of {@link groovy.xml.MarkupBuilder}
+ */
+ Closure renderOutput(DomainProperty property)
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRendererRegistry.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRendererRegistry.groovy
new file mode 100644
index 00000000..43f885ce
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/DomainOutputRendererRegistry.groovy
@@ -0,0 +1,13 @@
+package org.grails.scaffolding.registry
+
+import groovy.transform.CompileStatic
+
+/**
+ * A registry of {@link DomainOutputRenderer} instances
+ *
+ * @author James Kleeh
+ */
+@CompileStatic
+class DomainOutputRendererRegistry extends DomainRendererRegistry {
+
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainRenderer.groovy
new file mode 100644
index 00000000..bcb7729d
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/DomainRenderer.groovy
@@ -0,0 +1,20 @@
+package org.grails.scaffolding.registry
+
+import org.grails.scaffolding.model.property.DomainProperty
+
+/**
+ * Used to render markup for a domain class property
+ *
+ * @author James Kleeh
+ */
+interface DomainRenderer {
+
+ /**
+ * Determines if the renderer supports rendering the given property
+ *
+ * @param property The domain property to be rendered
+ * @return Whether or not the property is supported
+ */
+ boolean supports(DomainProperty property)
+
+}
\ No newline at end of file
diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegisterer.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegisterer.groovy
new file mode 100644
index 00000000..584492bd
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegisterer.groovy
@@ -0,0 +1,48 @@
+package org.grails.scaffolding.registry
+
+import org.grails.scaffolding.registry.input.*
+import org.grails.scaffolding.registry.output.DefaultOutputRenderer
+import grails.web.mapping.LinkGenerator
+import groovy.transform.CompileStatic
+import org.springframework.beans.factory.annotation.Autowired
+import jakarta.annotation.PostConstruct
+
+/**
+ * Bean for registering the default domain renderers
+ *
+ * @author James Kleeh
+ */
+@CompileStatic
+class DomainRendererRegisterer {
+
+ @Autowired
+ DomainInputRendererRegistry domainInputRendererRegistry
+
+ @Autowired
+ DomainOutputRendererRegistry domainOutputRendererRegistry
+
+ @Autowired
+ LinkGenerator grailsLinkGenerator
+
+ @PostConstruct
+ void registerRenderers() {
+ domainInputRendererRegistry.registerDomainRenderer(new DefaultInputRenderer(), -3)
+ domainInputRendererRegistry.registerDomainRenderer(new UrlInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new TimeZoneInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new TimeInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new StringInputRenderer(), -2)
+ domainInputRendererRegistry.registerDomainRenderer(new TextareaInputRenderer(), -2)
+ domainInputRendererRegistry.registerDomainRenderer(new NumberInputRenderer(), -2)
+ domainInputRendererRegistry.registerDomainRenderer(new LocaleInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new InListInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new FileInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new EnumInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new DateInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new CurrencyInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new BooleanInputRenderer(), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new BidirectionalToManyInputRenderer(grailsLinkGenerator), -1)
+ domainInputRendererRegistry.registerDomainRenderer(new AssociationInputRenderer(), -2)
+
+ domainOutputRendererRegistry.registerDomainRenderer(new DefaultOutputRenderer(), -1)
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegistry.groovy b/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegistry.groovy
new file mode 100644
index 00000000..c860b2a0
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/DomainRendererRegistry.groovy
@@ -0,0 +1,52 @@
+package org.grails.scaffolding.registry
+
+import org.grails.scaffolding.model.property.DomainProperty
+import groovy.transform.CompileStatic
+
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * A registry of domain property renderers sorted by priority and order of addition
+ *
+ * @author James Kleeh
+ */
+@CompileStatic
+abstract class DomainRendererRegistry {
+
+ protected SortedSet domainRegistryEntries = new TreeSet();
+
+ protected final AtomicInteger RENDERER_SEQUENCE = new AtomicInteger(0);
+
+ void registerDomainRenderer(T domainRenderer, Integer priority) {
+ domainRegistryEntries.add(new Entry(domainRenderer, priority))
+ }
+
+ public SortedSet getDomainRegistryEntries() {
+ this.domainRegistryEntries
+ }
+
+ T get(DomainProperty domainProperty) {
+ for (Entry entry : domainRegistryEntries) {
+ if (entry.renderer.supports(domainProperty)) {
+ return entry.renderer
+ }
+ }
+ null
+ }
+
+ private class Entry implements Comparable {
+ protected final T renderer
+ private final int priority;
+ private final int seq;
+
+ Entry(T renderer, int priority) {
+ this.renderer = renderer
+ this.priority = priority
+ seq = RENDERER_SEQUENCE.incrementAndGet()
+ }
+
+ public int compareTo(Entry entry) {
+ return priority == entry.priority ? entry.seq - seq : entry.priority - priority;
+ }
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/AssociationInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/AssociationInputRenderer.groovy
new file mode 100644
index 00000000..3d225f6c
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/input/AssociationInputRenderer.groovy
@@ -0,0 +1,23 @@
+package org.grails.scaffolding.registry.input
+
+import org.grails.scaffolding.model.property.DomainProperty
+import org.grails.scaffolding.registry.DomainInputRenderer
+import org.grails.datastore.mapping.model.types.Association
+
+/**
+ * The default renderer for rendering associations
+ *
+ * @author James Kleeh
+ */
+class AssociationInputRenderer implements DomainInputRenderer {
+
+ @Override
+ boolean supports(DomainProperty property) {
+ property.persistentProperty instanceof Association
+ }
+
+ @Override
+ Closure renderInput(Map defaultAttributes, DomainProperty property) {
+ { -> }
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRenderer.groovy
new file mode 100644
index 00000000..0ea2a08c
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/input/BidirectionalToManyInputRenderer.groovy
@@ -0,0 +1,47 @@
+package org.grails.scaffolding.registry.input
+
+import org.grails.scaffolding.model.property.DomainProperty
+import org.grails.scaffolding.registry.DomainInputRenderer
+import grails.util.GrailsNameUtils
+import grails.web.mapping.LinkGenerator
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.model.types.ToMany
+
+/**
+ * The default renderer for rendering bidirectional to many associations
+ *
+ * @author James Kleeh
+ */
+class BidirectionalToManyInputRenderer implements DomainInputRenderer {
+
+ protected LinkGenerator linkGenerator
+
+ BidirectionalToManyInputRenderer(LinkGenerator linkGenerator) {
+ this.linkGenerator = linkGenerator
+ }
+
+ @Override
+ boolean supports(DomainProperty property) {
+ PersistentProperty persistentProperty = property.persistentProperty
+ persistentProperty instanceof ToMany && persistentProperty.bidirectional
+ }
+
+ protected String getPropertyName(DomainProperty property) {
+ GrailsNameUtils.getPropertyName(property.rootBeanType)
+ }
+
+ protected String getAssociatedClassName(DomainProperty property) {
+ property.associatedType.simpleName
+ }
+
+ @Override
+ Closure renderInput(Map defaultAttributes, DomainProperty property) {
+ final String objectName = "${getPropertyName(property)}.id"
+ defaultAttributes.remove('required')
+ defaultAttributes.remove('readonly')
+ defaultAttributes.href = linkGenerator.link(resource: property.associatedType, action: "create", params: [(objectName): ""])
+ return { ->
+ a("Add ${getAssociatedClassName(property)}", defaultAttributes)
+ }
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/BooleanInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/BooleanInputRenderer.groovy
new file mode 100644
index 00000000..1e9389f0
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/input/BooleanInputRenderer.groovy
@@ -0,0 +1,25 @@
+package org.grails.scaffolding.registry.input
+
+import org.grails.scaffolding.model.property.DomainProperty
+import org.grails.scaffolding.registry.DomainInputRenderer
+
+/**
+ * The default renderer for rendering boolean properties
+ *
+ * @author James Kleeh
+ */
+class BooleanInputRenderer implements DomainInputRenderer {
+
+ @Override
+ boolean supports(DomainProperty domainProperty) {
+ domainProperty.type in [boolean, Boolean]
+ }
+
+ @Override
+ Closure renderInput(Map standardAttributes, DomainProperty domainProperty) {
+ standardAttributes.type = "checkbox"
+ return { ->
+ input(standardAttributes)
+ }
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/CurrencyInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/CurrencyInputRenderer.groovy
new file mode 100644
index 00000000..cfe4e2bf
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/input/CurrencyInputRenderer.groovy
@@ -0,0 +1,44 @@
+package org.grails.scaffolding.registry.input
+
+import org.grails.scaffolding.model.property.DomainProperty
+import org.grails.scaffolding.registry.DomainInputRenderer
+import groovy.transform.CompileStatic
+
+/**
+ * The default renderer for rendering {@link Currency} properties
+ *
+ * @author James Kleeh
+ */
+@CompileStatic
+class CurrencyInputRenderer implements MapToSelectInputRenderer {
+
+ String getOptionValue(Currency currency) {
+ currency.currencyCode
+ }
+
+ String getOptionKey(Currency currency) {
+ currency.currencyCode
+ }
+
+ protected List getDefaultCurrencyCodes() {
+ ['EUR', 'XCD', 'USD', 'XOF', 'NOK', 'AUD',
+ 'XAF', 'NZD', 'MAD', 'DKK', 'GBP', 'CHF',
+ 'XPF', 'ILS', 'ROL', 'TRL']
+ }
+
+ Map getOptions() {
+ defaultCurrencyCodes.collectEntries {
+ Currency currency = Currency.getInstance(it)
+ [(getOptionKey(currency)): getOptionValue(currency)]
+ }
+ }
+
+ Currency getDefaultOption() {
+ Currency.getInstance(Locale.default)
+ }
+
+ @Override
+ boolean supports(DomainProperty property) {
+ property.type in Currency
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/DateInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/DateInputRenderer.groovy
new file mode 100644
index 00000000..6dc167e0
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/input/DateInputRenderer.groovy
@@ -0,0 +1,26 @@
+package org.grails.scaffolding.registry.input
+
+import org.grails.scaffolding.model.property.DomainProperty
+import org.grails.scaffolding.registry.DomainInputRenderer
+
+/**
+ * The default renderer for rendering date properties
+ *
+ * @author James Kleeh
+ */
+class DateInputRenderer implements DomainInputRenderer {
+
+ @Override
+ boolean supports(DomainProperty property) {
+ property.type in [Date, Calendar, java.sql.Date]
+ }
+
+ @Override
+ Closure renderInput(Map defaultAttributes, DomainProperty property) {
+ defaultAttributes.type = "date"
+ defaultAttributes.placeholder = "YYYY-MM-DD"
+ return { ->
+ input(defaultAttributes)
+ }
+ }
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/DefaultInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/DefaultInputRenderer.groovy
new file mode 100644
index 00000000..dda16f83
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/input/DefaultInputRenderer.groovy
@@ -0,0 +1,24 @@
+package org.grails.scaffolding.registry.input
+
+import org.grails.scaffolding.model.property.DomainProperty
+import org.grails.scaffolding.registry.DomainInputRenderer
+
+/**
+ * The renderer chosen for outputting domain properties when no other
+ * renderers support the given property
+ *
+ * @author James Kleeh
+ */
+class DefaultInputRenderer implements DomainInputRenderer {
+
+ @Override
+ boolean supports(DomainProperty property) {
+ true
+ }
+
+ @Override
+ Closure renderInput(Map attributes, DomainProperty property) {
+ { -> }
+ }
+
+}
diff --git a/src/main/groovy/org/grails/scaffolding/registry/input/EnumInputRenderer.groovy b/src/main/groovy/org/grails/scaffolding/registry/input/EnumInputRenderer.groovy
new file mode 100644
index 00000000..26f1f540
--- /dev/null
+++ b/src/main/groovy/org/grails/scaffolding/registry/input/EnumInputRenderer.groovy
@@ -0,0 +1,40 @@
+package org.grails.scaffolding.registry.input
+
+import org.grails.scaffolding.model.property.DomainProperty
+import org.grails.scaffolding.registry.DomainInputRenderer
+
+/**
+ * The default renderer for rendering enum properties
+ *
+ * @author James Kleeh
+ */
+class EnumInputRenderer implements DomainInputRenderer {
+
+ protected List