Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

a helper for getting bindable properties of domain types

  • Loading branch information...
commit 1ca62c87457c1fe9a9999090c89fbd19d19dd3f9 1 parent c82c8b5
Rob Fletcher authored
1  .gitignore
@@ -10,6 +10,7 @@
10 10 .DS_Store
11 11
12 12 # IntelliJ project files
  13 +/.idea
13 14 /*.iws
14 15 /*.iml
15 16 /*.ipr
3  TODO.md
Source Rendered
... ... @@ -0,0 +1,3 @@
  1 +- i18n
  2 +- flash messages
  3 +- use proper REST URL scheme
6 grails-app/conf/BuildConfig.groovy
@@ -21,10 +21,14 @@ grails.project.dependency.resolution = {
21 21 }
22 22
23 23 runtime(":hibernate:$grailsVersion",
24   - ':resources:1.0-RC2',
  24 + ':resources:1.1.6',
25 25 ':jquery:1.7.2',
26 26 ':ember:0.9.8.1') {
27 27 export = false
28 28 }
  29 +
  30 + test(':spock:0.6') {
  31 + export = false
  32 + }
29 33 }
30 34 }
23 src/groovy/grails/plugin/emberscaffolding/ScaffoldingHelper.groovy
... ... @@ -0,0 +1,23 @@
  1 +package grails.plugin.emberscaffolding
  2 +
  3 +import org.codehaus.groovy.grails.web.binding.DefaultASTDatabindingHelper
  4 +
  5 +class ScaffoldingHelper {
  6 +
  7 + static List<String> getBindableProperties(Class domainClass) {
  8 + domainClass."$DefaultASTDatabindingHelper.DEFAULT_DATABINDING_WHITELIST".findAll {
  9 + !it.endsWith('_*')
  10 + }
  11 + }
  12 +
  13 + static Map<String, Object> getBindablePropertyValues(domainInstance) {
  14 + domainInstance.properties.getAt(getBindableProperties(domainInstance.getClass()))
  15 + }
  16 +
  17 + static Collection<Map<String, Object>> getBindablePropertyValues(Collection domainInstances) {
  18 + domainInstances.collect {
  19 + getBindablePropertyValues(it)
  20 + }
  21 + }
  22 +
  23 +}
182 src/templates/scaffolding/Controller.groovy
... ... @@ -1,101 +1,103 @@
1 1 <%=packageName ? "package ${packageName}\n\n" : ''%>import org.springframework.dao.DataIntegrityViolationException
  2 +import grails.converters.JSON
  3 +import grails.plugin.emberscaffolding.ScaffoldingHelper
2 4
3 5 class ${className}Controller {
4 6
5   - static allowedMethods = [save: "POST", update: "POST", delete: "POST"]
  7 + static scaffold = true
6 8
7   - def index() {
8   - redirect(action: "list", params: params)
9   - }
  9 + static allowedMethods = [save: 'POST', update: 'POST', delete: 'POST']
  10 +
  11 + def index() {}
10 12
11 13 def list() {
12 14 params.max = Math.min(params.max ? params.int('max') : 10, 100)
13   - [${propertyName}List: ${className}.list(params), ${propertyName}Total: ${className}.count()]
14   - }
15   -
16   - def create() {
17   - [${propertyName}: new ${className}(params)]
  15 + render ScaffoldingHelper.getBindablePropertyValues(${className}.list(params)) as JSON
18 16 }
19 17
20   - def save() {
21   - def ${propertyName} = new ${className}(params)
22   - if (!${propertyName}.save(flush: true)) {
23   - render(view: "create", model: [${propertyName}: ${propertyName}])
24   - return
25   - }
26   -
27   - flash.message = message(code: 'default.created.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), ${propertyName}.id])
28   - redirect(action: "show", id: ${propertyName}.id)
29   - }
30   -
31   - def show() {
32   - def ${propertyName} = ${className}.get(params.id)
33   - if (!${propertyName}) {
34   - flash.message = message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
35   - redirect(action: "list")
36   - return
37   - }
38   -
39   - [${propertyName}: ${propertyName}]
40   - }
41   -
42   - def edit() {
43   - def ${propertyName} = ${className}.get(params.id)
44   - if (!${propertyName}) {
45   - flash.message = message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
46   - redirect(action: "list")
47   - return
48   - }
49   -
50   - [${propertyName}: ${propertyName}]
51   - }
52   -
53   - def update() {
54   - def ${propertyName} = ${className}.get(params.id)
55   - if (!${propertyName}) {
56   - flash.message = message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
57   - redirect(action: "list")
58   - return
59   - }
60   -
61   - if (params.version) {
62   - def version = params.version.toLong()
63   - if (${propertyName}.version > version) {<% def lowerCaseName = grails.util.GrailsNameUtils.getPropertyName(className) %>
64   - ${propertyName}.errors.rejectValue("version", "default.optimistic.locking.failure",
65   - [message(code: '${domainClass.propertyName}.label', default: '${className}')] as Object[],
66   - "Another user has updated this ${className} while you were editing")
67   - render(view: "edit", model: [${propertyName}: ${propertyName}])
68   - return
69   - }
70   - }
71   -
72   - ${propertyName}.properties = params
73   -
74   - if (!${propertyName}.save(flush: true)) {
75   - render(view: "edit", model: [${propertyName}: ${propertyName}])
76   - return
77   - }
78   -
79   - flash.message = message(code: 'default.updated.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), ${propertyName}.id])
80   - redirect(action: "show", id: ${propertyName}.id)
81   - }
82   -
83   - def delete() {
84   - def ${propertyName} = ${className}.get(params.id)
85   - if (!${propertyName}) {
86   - flash.message = message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
87   - redirect(action: "list")
88   - return
89   - }
90   -
91   - try {
92   - ${propertyName}.delete(flush: true)
93   - flash.message = message(code: 'default.deleted.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
94   - redirect(action: "list")
95   - }
96   - catch (DataIntegrityViolationException e) {
97   - flash.message = message(code: 'default.not.deleted.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
98   - redirect(action: "show", id: params.id)
99   - }
100   - }
  18 +// def create() {
  19 +// [${propertyName}: new ${className}(params)]
  20 +// }
  21 +//
  22 +// def save() {
  23 +// def ${propertyName} = new ${className}(params)
  24 +// if (!${propertyName}.save(flush: true)) {
  25 +// render(view: "create", model: [${propertyName}: ${propertyName}])
  26 +// return
  27 +// }
  28 +//
  29 +// flash.message = message(code: 'default.created.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), ${propertyName}.id])
  30 +// redirect(action: "show", id: ${propertyName}.id)
  31 +// }
  32 +//
  33 +// def show() {
  34 +// def ${propertyName} = ${className}.get(params.id)
  35 +// if (!${propertyName}) {
  36 +// flash.message = message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
  37 +// redirect(action: "list")
  38 +// return
  39 +// }
  40 +//
  41 +// [${propertyName}: ${propertyName}]
  42 +// }
  43 +//
  44 +// def edit() {
  45 +// def ${propertyName} = ${className}.get(params.id)
  46 +// if (!${propertyName}) {
  47 +// flash.message = message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
  48 +// redirect(action: "list")
  49 +// return
  50 +// }
  51 +//
  52 +// [${propertyName}: ${propertyName}]
  53 +// }
  54 +//
  55 +// def update() {
  56 +// def ${propertyName} = ${className}.get(params.id)
  57 +// if (!${propertyName}) {
  58 +// flash.message = message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
  59 +// redirect(action: "list")
  60 +// return
  61 +// }
  62 +//
  63 +// if (params.version) {
  64 +// def version = params.version.toLong()
  65 +// if (${propertyName}.version > version) {<% def lowerCaseName = grails.util.GrailsNameUtils.getPropertyName(className) %>
  66 +// ${propertyName}.errors.rejectValue("version", "default.optimistic.locking.failure",
  67 +// [message(code: '${domainClass.propertyName}.label', default: '${className}')] as Object[],
  68 +// "Another user has updated this ${className} while you were editing")
  69 +// render(view: "edit", model: [${propertyName}: ${propertyName}])
  70 +// return
  71 +// }
  72 +// }
  73 +//
  74 +// ${propertyName}.properties = params
  75 +//
  76 +// if (!${propertyName}.save(flush: true)) {
  77 +// render(view: "edit", model: [${propertyName}: ${propertyName}])
  78 +// return
  79 +// }
  80 +//
  81 +// flash.message = message(code: 'default.updated.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), ${propertyName}.id])
  82 +// redirect(action: "show", id: ${propertyName}.id)
  83 +// }
  84 +//
  85 +// def delete() {
  86 +// def ${propertyName} = ${className}.get(params.id)
  87 +// if (!${propertyName}) {
  88 +// flash.message = message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
  89 +// redirect(action: "list")
  90 +// return
  91 +// }
  92 +//
  93 +// try {
  94 +// ${propertyName}.delete(flush: true)
  95 +// flash.message = message(code: 'default.deleted.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
  96 +// redirect(action: "list")
  97 +// }
  98 +// catch (DataIntegrityViolationException e) {
  99 +// flash.message = message(code: 'default.not.deleted.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])
  100 +// redirect(action: "show", id: params.id)
  101 +// }
  102 +// }
101 103 }
157 src/templates/scaffolding/Test.groovy
... ... @@ -1,157 +0,0 @@
1   -<%=packageName ? "package ${packageName}\n\n" : ''%>
2   -
3   -import org.junit.*
4   -import grails.test.mixin.*
5   -
6   -@TestFor(${className}Controller)
7   -@Mock(${className})
8   -class ${className}ControllerTests {
9   -
10   -
11   - def populateValidParams(params) {
12   - assert params != null
13   - // TODO: Populate valid properties like...
14   - //params["name"] = 'someValidName'
15   - }
16   -
17   - void testIndex() {
18   - controller.index()
19   - assert "/$propertyName/list" == response.redirectedUrl
20   - }
21   -
22   - void testList() {
23   -
24   - def model = controller.list()
25   -
26   - assert model.${propertyName}InstanceList.size() == 0
27   - assert model.${propertyName}InstanceTotal == 0
28   - }
29   -
30   - void testCreate() {
31   - def model = controller.create()
32   -
33   - assert model.${propertyName}Instance != null
34   - }
35   -
36   - void testSave() {
37   - controller.save()
38   -
39   - assert model.${propertyName}Instance != null
40   - assert view == '/${propertyName}/create'
41   -
42   - response.reset()
43   -
44   - populateValidParams(params)
45   - controller.save()
46   -
47   - assert response.redirectedUrl == '/${propertyName}/show/1'
48   - assert controller.flash.message != null
49   - assert ${className}.count() == 1
50   - }
51   -
52   - void testShow() {
53   - controller.show()
54   -
55   - assert flash.message != null
56   - assert response.redirectedUrl == '/${propertyName}/list'
57   -
58   -
59   - populateValidParams(params)
60   - def ${propertyName} = new ${className}(params)
61   -
62   - assert ${propertyName}.save() != null
63   -
64   - params.id = ${propertyName}.id
65   -
66   - def model = controller.show()
67   -
68   - assert model.${propertyName}Instance == ${propertyName}
69   - }
70   -
71   - void testEdit() {
72   - controller.edit()
73   -
74   - assert flash.message != null
75   - assert response.redirectedUrl == '/${propertyName}/list'
76   -
77   -
78   - populateValidParams(params)
79   - def ${propertyName} = new ${className}(params)
80   -
81   - assert ${propertyName}.save() != null
82   -
83   - params.id = ${propertyName}.id
84   -
85   - def model = controller.edit()
86   -
87   - assert model.${propertyName}Instance == ${propertyName}
88   - }
89   -
90   - void testUpdate() {
91   - controller.update()
92   -
93   - assert flash.message != null
94   - assert response.redirectedUrl == '/${propertyName}/list'
95   -
96   - response.reset()
97   -
98   -
99   - populateValidParams(params)
100   - def ${propertyName} = new ${className}(params)
101   -
102   - assert ${propertyName}.save() != null
103   -
104   - // test invalid parameters in update
105   - params.id = ${propertyName}.id
106   - //TODO: add invalid values to params object
107   -
108   - controller.update()
109   -
110   - assert view == "/${propertyName}/edit"
111   - assert model.${propertyName}Instance != null
112   -
113   - ${propertyName}.clearErrors()
114   -
115   - populateValidParams(params)
116   - controller.update()
117   -
118   - assert response.redirectedUrl == "/${propertyName}/show/\$${propertyName}.id"
119   - assert flash.message != null
120   -
121   - //test outdated version number
122   - response.reset()
123   - ${propertyName}.clearErrors()
124   -
125   - populateValidParams(params)
126   - params.id = ${propertyName}.id
127   - params.version = -1
128   - controller.update()
129   -
130   - assert view == "/${propertyName}/edit"
131   - assert model.${propertyName}Instance != null
132   - assert model.${propertyName}Instance.errors.getFieldError('version')
133   - assert flash.message != null
134   - }
135   -
136   - void testDelete() {
137   - controller.delete()
138   - assert flash.message != null
139   - assert response.redirectedUrl == '/${propertyName}/list'
140   -
141   - response.reset()
142   -
143   - populateValidParams(params)
144   - def ${propertyName} = new ${className}(params)
145   -
146   - assert ${propertyName}.save() != null
147   - assert ${className}.count() == 1
148   -
149   - params.id = ${propertyName}.id
150   -
151   - controller.delete()
152   -
153   - assert ${className}.count() == 0
154   - assert ${className}.get(${propertyName}.id) == null
155   - assert response.redirectedUrl == '/${propertyName}/list'
156   - }
157   -}
44 src/templates/scaffolding/_form.gsp
... ... @@ -1,44 +0,0 @@
1   -<%=packageName%>
2   -<% import grails.persistence.Event %>
3   -
4   -<% excludedProps = Event.allEvents.toList() << 'version' << 'dateCreated' << 'lastUpdated'
5   - persistentPropNames = domainClass.persistentProperties*.name
6   - boolean hasHibernate = pluginManager?.hasGrailsPlugin('hibernate')
7   - if (hasHibernate && org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsDomainBinder.getMapping(domainClass)?.identity?.generator == 'assigned') {
8   - persistentPropNames << domainClass.identifier.name
9   - }
10   - props = domainClass.properties.findAll { persistentPropNames.contains(it.name) && !excludedProps.contains(it.name) }
11   - Collections.sort(props, comparator.constructors[0].newInstance([domainClass] as Object[]))
12   - for (p in props) {
13   - if (p.embedded) {
14   - def embeddedPropNames = p.component.persistentProperties*.name
15   - def embeddedProps = p.component.properties.findAll { embeddedPropNames.contains(it.name) && !excludedProps.contains(it.name) }
16   - Collections.sort(embeddedProps, comparator.constructors[0].newInstance([p.component] as Object[]))
17   - %><fieldset class="embedded"><legend><g:message code="${domainClass.propertyName}.${p.name}.label" default="${p.naturalName}" /></legend><%
18   - for (ep in p.component.properties) {
19   - renderFieldForProperty(ep, p.component, "${p.name}.")
20   - }
21   - %></fieldset><%
22   - } else {
23   - renderFieldForProperty(p, domainClass)
24   - }
25   - }
26   -
27   -private renderFieldForProperty(p, owningClass, prefix = "") {
28   - boolean hasHibernate = pluginManager?.hasGrailsPlugin('hibernate')
29   - boolean display = true
30   - boolean required = false
31   - if (hasHibernate) {
32   - cp = owningClass.constrainedProperties[p.name]
33   - display = (cp ? cp.display : true)
34   - required = (cp ? !(cp.propertyType in [boolean, Boolean]) && !cp.nullable && (cp.propertyType != String || !cp.blank) : false)
35   - }
36   - if (display) { %>
37   -<div class="fieldcontain \${hasErrors(bean: ${propertyName}, field: '${prefix}${p.name}', 'error')} ${required ? 'required' : ''}">
38   - <label for="${prefix}${p.name}">
39   - <g:message code="${domainClass.propertyName}.${prefix}${p.name}.label" default="${p.naturalName}" />
40   - <% if (required) { %><span class="required-indicator">*</span><% } %>
41   - </label>
42   - ${renderEditor(p)}
43   -</div>
44   -<% } } %>
39 src/templates/scaffolding/create.gsp
... ... @@ -1,39 +0,0 @@
1   -<%=packageName%>
2   -<!doctype html>
3   -<html>
4   - <head>
5   - <meta name="layout" content="main">
6   - <g:set var="entityName" value="\${message(code: '${domainClass.propertyName}.label', default: '${className}')}" />
7   - <title><g:message code="default.create.label" args="[entityName]" /></title>
8   - </head>
9   - <body>
10   - <a href="#create-${domainClass.propertyName}" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
11   - <div class="nav" role="navigation">
12   - <ul>
13   - <li><a class="home" href="\${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
14   - <li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
15   - </ul>
16   - </div>
17   - <div id="create-${domainClass.propertyName}" class="content scaffold-create" role="main">
18   - <h1><g:message code="default.create.label" args="[entityName]" /></h1>
19   - <g:if test="\${flash.message}">
20   - <div class="message" role="status">\${flash.message}</div>
21   - </g:if>
22   - <g:hasErrors bean="\${${propertyName}}">
23   - <ul class="errors" role="alert">
24   - <g:eachError bean="\${${propertyName}}" var="error">
25   - <li <g:if test="\${error in org.springframework.validation.FieldError}">data-field-id="\${error.field}"</g:if>><g:message error="\${error}"/></li>
26   - </g:eachError>
27   - </ul>
28   - </g:hasErrors>
29   - <g:form action="save" <%= multiPart ? ' enctype="multipart/form-data"' : '' %>>
30   - <fieldset class="form">
31   - <g:render template="form"/>
32   - </fieldset>
33   - <fieldset class="buttons">
34   - <g:submitButton name="create" class="save" value="\${message(code: 'default.button.create.label', default: 'Create')}" />
35   - </fieldset>
36   - </g:form>
37   - </div>
38   - </body>
39   -</html>
43 src/templates/scaffolding/edit.gsp
... ... @@ -1,43 +0,0 @@
1   -<%=packageName%>
2   -<!doctype html>
3   -<html>
4   - <head>
5   - <meta name="layout" content="main">
6   - <g:set var="entityName" value="\${message(code: '${domainClass.propertyName}.label', default: '${className}')}" />
7   - <title><g:message code="default.edit.label" args="[entityName]" /></title>
8   - </head>
9   - <body>
10   - <a href="#edit-${domainClass.propertyName}" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
11   - <div class="nav" role="navigation">
12   - <ul>
13   - <li><a class="home" href="\${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
14   - <li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
15   - <li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
16   - </ul>
17   - </div>
18   - <div id="edit-${domainClass.propertyName}" class="content scaffold-edit" role="main">
19   - <h1><g:message code="default.edit.label" args="[entityName]" /></h1>
20   - <g:if test="\${flash.message}">
21   - <div class="message" role="status">\${flash.message}</div>
22   - </g:if>
23   - <g:hasErrors bean="\${${propertyName}}">
24   - <ul class="errors" role="alert">
25   - <g:eachError bean="\${${propertyName}}" var="error">
26   - <li <g:if test="\${error in org.springframework.validation.FieldError}">data-field-id="\${error.field}"</g:if>><g:message error="\${error}"/></li>
27   - </g:eachError>
28   - </ul>
29   - </g:hasErrors>
30   - <g:form method="post" <%= multiPart ? ' enctype="multipart/form-data"' : '' %>>
31   - <g:hiddenField name="id" value="\${${propertyName}?.id}" />
32   - <g:hiddenField name="version" value="\${${propertyName}?.version}" />
33   - <fieldset class="form">
34   - <g:render template="form"/>
35   - </fieldset>
36   - <fieldset class="buttons">
37   - <g:actionSubmit class="save" action="update" value="\${message(code: 'default.button.update.label', default: 'Update')}" />
38   - <g:actionSubmit class="delete" action="delete" value="\${message(code: 'default.button.delete.label', default: 'Delete')}" formnovalidate="" onclick="return confirm('\${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
39   - </fieldset>
40   - </g:form>
41   - </div>
42   - </body>
43   -</html>
25 src/templates/scaffolding/index.gsp
... ... @@ -0,0 +1,25 @@
  1 +<% import grails.persistence.Event %>
  2 +<%=packageName%>
  3 +<!doctype html>
  4 +<html>
  5 + <head>
  6 + <meta name="layout" content="main">
  7 + <g:set var="entityName" value="\${message(code: '${domainClass.propertyName}.label', default: '${className}')}" />
  8 + <title>\${entityName}</title>
  9 + <r:require module="ember"/>
  10 + </head>
  11 + <body>
  12 + <div class="nav" role="navigation">
  13 + <ul>
  14 + <li><a class="home" href="\${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
  15 + <li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
  16 + <li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
  17 + </ul>
  18 + </div>
  19 + <div class="content" role="main">
  20 + </div>
  21 + <script type="text/x-handlebars" data-template-name="\${entityName}-list">
  22 + <g:include view="list.handlebars"/>
  23 + </script>
  24 + </body>
  25 +</html>
62 src/templates/scaffolding/list.gsp
... ... @@ -1,62 +0,0 @@
1   -<% import grails.persistence.Event %>
2   -<%=packageName%>
3   -<!doctype html>
4   -<html>
5   - <head>
6   - <meta name="layout" content="main">
7   - <g:set var="entityName" value="\${message(code: '${domainClass.propertyName}.label', default: '${className}')}" />
8   - <title><g:message code="default.list.label" args="[entityName]" /></title>
9   - </head>
10   - <body>
11   - <a href="#list-${domainClass.propertyName}" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
12   - <div class="nav" role="navigation">
13   - <ul>
14   - <li><a class="home" href="\${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
15   - <li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
16   - </ul>
17   - </div>
18   - <div id="list-${domainClass.propertyName}" class="content scaffold-list" role="main">
19   - <h1><g:message code="default.list.label" args="[entityName]" /></h1>
20   - <g:if test="\${flash.message}">
21   - <div class="message" role="status">\${flash.message}</div>
22   - </g:if>
23   - <table>
24   - <thead>
25   - <tr>
26   - <% excludedProps = Event.allEvents.toList() << 'id' << 'version'
27   - allowedNames = domainClass.persistentProperties*.name << 'dateCreated' << 'lastUpdated'
28   - props = domainClass.properties.findAll { allowedNames.contains(it.name) && !excludedProps.contains(it.name) && it.type != null && !Collection.isAssignableFrom(it.type) }
29   - Collections.sort(props, comparator.constructors[0].newInstance([domainClass] as Object[]))
30   - props.eachWithIndex { p, i ->
31   - if (i < 6) {
32   - if (p.isAssociation()) { %>
33   - <th><g:message code="${domainClass.propertyName}.${p.name}.label" default="${p.naturalName}" /></th>
34   - <% } else { %>
35   - <g:sortableColumn property="${p.name}" title="\${message(code: '${domainClass.propertyName}.${p.name}.label', default: '${p.naturalName}')}" />
36   - <% } } } %>
37   - </tr>
38   - </thead>
39   - <tbody>
40   - <g:each in="\${${propertyName}List}" status="i" var="${propertyName}">
41   - <tr class="\${(i % 2) == 0 ? 'even' : 'odd'}">
42   - <% props.eachWithIndex { p, i ->
43   - if (i == 0) { %>
44   - <td><g:link action="show" id="\${${propertyName}.id}">\${fieldValue(bean: ${propertyName}, field: "${p.name}")}</g:link></td>
45   - <% } else if (i < 6) {
46   - if (p.type == Boolean || p.type == boolean) { %>
47   - <td><g:formatBoolean boolean="\${${propertyName}.${p.name}}" /></td>
48   - <% } else if (p.type == Date || p.type == java.sql.Date || p.type == java.sql.Time || p.type == Calendar) { %>
49   - <td><g:formatDate date="\${${propertyName}.${p.name}}" /></td>
50   - <% } else { %>
51   - <td>\${fieldValue(bean: ${propertyName}, field: "${p.name}")}</td>
52   - <% } } } %>
53   - </tr>
54   - </g:each>
55   - </tbody>
56   - </table>
57   - <div class="pagination">
58   - <g:paginate total="\${${propertyName}Total}" />
59   - </div>
60   - </div>
61   - </body>
62   -</html>
18 src/templates/scaffolding/list.handlebars
... ... @@ -0,0 +1,18 @@
  1 +<% import grails.persistence.Event %>
  2 +<h1>List</h1>
  3 +<table>
  4 + <thead>
  5 + <tr>
  6 + <% excludedProps = Event.allEvents.toList() << 'id' << 'version'
  7 + allowedNames = domainClass.persistentProperties*.name << 'dateCreated' << 'lastUpdated'
  8 + props = domainClass.properties.findAll { allowedNames.contains(it.name) && !excludedProps.contains(it.name) && it.type != null && !Collection.isAssignableFrom(it.type) }
  9 + Collections.sort(props, comparator.constructors[0].newInstance([domainClass] as Object[]))
  10 + props.eachWithIndex { p, i ->
  11 + <th><g:message code="${domainClass.propertyName}.${p.name}.label" default="${p.naturalName}" /></th>
  12 + } %>
  13 + </tr>
  14 + </thead>
  15 + <tbody>
  16 + </g:each>
  17 + </tbody>
  18 +</table>
266 src/templates/scaffolding/renderEditor.template
... ... @@ -1,266 +0,0 @@
1   -<% if (property.type == Boolean || property.type == boolean)
2   - out << renderBooleanEditor(domainClass, property)
3   - else if (property.type && Number.isAssignableFrom(property.type) || (property.type?.isPrimitive() && property.type != boolean))
4   - out << renderNumberEditor(domainClass, property)
5   - else if (property.type == String)
6   - out << renderStringEditor(domainClass, property)
7   - else if (property.type == Date || property.type == java.sql.Date || property.type == java.sql.Time || property.type == Calendar)
8   - out << renderDateEditor(domainClass, property)
9   - else if (property.type == URL)
10   - out << renderStringEditor(domainClass, property)
11   - else if (property.type && property.isEnum())
12   - out << renderEnumEditor(domainClass, property)
13   - else if (property.type == TimeZone)
14   - out << renderSelectTypeEditor("timeZone", domainClass, property)
15   - else if (property.type == Locale)
16   - out << renderSelectTypeEditor("locale", domainClass, property)
17   - else if (property.type == Currency)
18   - out << renderSelectTypeEditor("currency", domainClass, property)
19   - else if (property.type==([] as Byte[]).class) //TODO: Bug in groovy means i have to do this :(
20   - out << renderByteArrayEditor(domainClass, property)
21   - else if (property.type==([] as byte[]).class) //TODO: Bug in groovy means i have to do this :(
22   - out << renderByteArrayEditor(domainClass, property)
23   - else if (property.manyToOne || property.oneToOne)
24   - out << renderManyToOne(domainClass, property)
25   - else if ((property.oneToMany && !property.bidirectional) || (property.manyToMany && property.isOwningSide())) {
26   - def str = renderManyToMany(domainClass, property)
27   - if(str != null) {
28   - out << str
29   - }
30   - }
31   - else if (property.oneToMany)
32   - out << renderOneToMany(domainClass, property)
33   -
34   - private renderEnumEditor(domainClass, property) {
35   - def sb = new StringBuilder("")
36   - sb << '<g:select name="' << property.name << '"'
37   - sb << ' from="${' << "${property.type.name}?.values()" << '}"'
38   - sb << ' keys="${' << property.type.name << '.values()*.name()}"'
39   - if (isRequired()) sb << ' required=""'
40   - sb << ' value="${' << "${domainInstance}?.${property.name}?.name()" << '}"'
41   - sb << renderNoSelection(property)
42   - sb << '/>'
43   - sb as String
44   - }
45   -
46   - private renderStringEditor(domainClass, property) {
47   - if (!cp) {
48   - return "<g:textField name=\"${property.name}\" value=\"\${${domainInstance}?.${property.name}}\" />"
49   - } else {
50   - def sb = new StringBuilder("")
51   - if ("textarea" == cp.widget || (cp.maxSize > 250 && !cp.password && !cp.inList)) {
52   - sb << '<g:textArea name="' << property.name << '"'
53   - sb << ' cols="40" rows="5"'
54   - if (cp.maxSize) sb << ' maxlength="' << cp.maxSize << '"'
55   - if (isRequired()) sb << ' required=""'
56   - sb << ' value="${' << "${domainInstance}?.${property.name}" << '}"'
57   - sb << '/>'
58   - } else if (cp.inList) {
59   - sb << '<g:select name="' << property.name << '"'
60   - sb << ' from="${' << "${domainInstance}.constraints.${property.name}.inList" << '}"'
61   - if (isRequired()) sb << ' required=""'
62   - sb << ' value="${' << "${domainInstance}?.${property.name}" << '}"'
63   - sb << ' valueMessagePrefix="' << "${domainClass.propertyName}.${property.name}" << '"'
64   - sb << renderNoSelection(property)
65   - sb << '/>'
66   - } else {
67   - if (cp.password) {
68   - sb << '<g:field type="password"'
69   - } else if (cp.url) {
70   - sb << '<g:field type="url"'
71   - } else if (cp.email) {
72   - sb << '<g:field type="email"'
73   - } else {
74   - sb << '<g:textField'
75   - }
76   - sb << ' name="' << property.name << '"'
77   - if (cp.maxSize) sb << ' maxlength="' << cp.maxSize << '"'
78   - if (!cp.editable) sb << ' readonly="readonly"'
79   - if (cp.matches) sb << ' pattern="${' << "${domainInstance}.constraints.${property.name}.matches" << '}"'
80   - if (isRequired()) sb << ' required=""'
81   - sb << ' value="${' << "${domainInstance}?.${property.name}" << '}"'
82   - sb << '/>'
83   - }
84   - sb as String
85   - }
86   - }
87   -
88   - private renderByteArrayEditor(domainClass, property) {
89   - return "<input type=\"file\" id=\"${property.name}\" name=\"${property.name}\" />"
90   - }
91   -
92   - private renderManyToOne(domainClass,property) {
93   - if (property.association) {
94   - def sb = new StringBuilder()
95   - sb << '<g:select'
96   - // id is "x" and name is "x.id" as the label will have for="x" and "." in an id will confuse CSS
97   - sb << ' id="' << property.name << '"'
98   - sb << ' name="' << property.name << '.id"'
99   - sb << ' from="${' << property.type.name << '.list()}"'
100   - sb << ' optionKey="id"'
101   - if (isRequired()) sb << ' required=""'
102   - sb << ' value="${' << "${domainInstance}?.${property.name}" << '?.id}"'
103   - sb << ' class="many-to-one"'
104   - sb << renderNoSelection(property)
105   - sb << '/>'
106   - sb as String
107   - }
108   - }
109   -
110   - private renderManyToMany(domainClass, property) {
111   - def cls = property.referencedDomainClass?.clazz
112   - if(cls == null) {
113   - if(property.type instanceof Collection) {
114   - cls = org.springframework.core.GenericCollectionTypeResolver.getCollectionType(property.type)
115   - }
116   - }
117   -
118   - if(cls != null) {
119   - def sb = new StringBuilder()
120   - sb << '<g:select name="' << property.name << '"'
121   - sb << ' from="${' << cls.name << '.list()}"'
122   - sb << ' multiple="multiple"'
123   - sb << ' optionKey="id"'
124   - sb << ' size="5"'
125   - if (isRequired()) sb << ' required=""'
126   - sb << ' value="${' << "${domainInstance}?.${property.name}" << '*.id}"'
127   - sb << ' class="many-to-many"'
128   - sb << '/>'
129   - sb as String
130   -
131   - }
132   -
133   - }
134   -
135   - private renderOneToMany(domainClass, property) {
136   - def sw = new StringWriter()
137   - def pw = new PrintWriter(sw)
138   - pw.println()
139   - pw.println '<ul class="one-to-many">'
140   - pw.println "<g:each in=\"\${${domainInstance}?.${property.name}?}\" var=\"${property.name[0]}\">"
141   - pw.println " <li><g:link controller=\"${property.referencedDomainClass.propertyName}\" action=\"show\" id=\"\${${property.name[0]}.id}\">\${${property.name[0]}?.encodeAsHTML()}</g:link></li>"
142   - pw.println '</g:each>'
143   - pw.println '<li class="add">'
144   - pw.println "<g:link controller=\"${property.referencedDomainClass.propertyName}\" action=\"create\" params=\"['${domainClass.propertyName}.id': ${domainInstance}?.id]\">\${message(code: 'default.add.label', args: [message(code: '${property.referencedDomainClass.propertyName}.label', default: '${property.referencedDomainClass.shortName}')])}</g:link>"
145   - pw.println '</li>'
146   - pw.println '</ul>'
147   - return sw.toString()
148   - }
149   -
150   - private renderNumberEditor(domainClass, property) {
151   - if (!cp) {
152   - if (property.type == Byte) {
153   - return "<g:select name=\"${property.name}\" from=\"\${-128..127}\" class=\"range\" value=\"\${fieldValue(bean: ${domainInstance}, field: '${property.name}')}\" />"
154   - } else {
155   - return "<g:field type=\"number\" name=\"${property.name}\" value=\"\${${domainInstance}.${property.name}}\" />"
156   - }
157   - } else {
158   - def sb = new StringBuilder()
159   - if (cp.range) {
160   - sb << '<g:select name="' << property.name << '"'
161   - sb << ' from="${' << "${cp.range.from}..${cp.range.to}" << '}"'
162   - sb << ' class="range"'
163   - if (isRequired()) sb << ' required=""'
164   - sb << ' value="${' << "fieldValue(bean: ${domainInstance}, field: '${property.name}')" << '}"'
165   - sb << renderNoSelection(property)
166   - sb << '/>'
167   - } else if (cp.inList) {
168   - sb << '<g:select name="' << property.name << '"'
169   - sb << ' from="${' << "${domainInstance}.constraints.${property.name}.inList" << '}"'
170   - if (isRequired()) sb << ' required=""'
171   - sb << ' value="${' << "fieldValue(bean: ${domainInstance}, field: '${property.name}')" << '}"'
172   - sb << ' valueMessagePrefix="' << "${domainClass.propertyName}.${property.name}" << '"'
173   - sb << renderNoSelection(property)
174   - sb << '/>'
175   - } else {
176   - sb << '<g:field type="number" name="' << property.name << '"'
177   - if (cp.scale != null && cp.scale < 7) sb << ' step="' << BigDecimal.valueOf(1).movePointLeft(cp.scale).toString() << '"'
178   - else if (property.type in [float, double, Float, Double, BigDecimal]) sb << ' step="any"'
179   - if (cp.min != null) sb << ' min="' << cp.min << '"'
180   - if (cp.max != null) sb << ' max="' << cp.max << '"'
181   - if (isRequired()) sb << ' required=""'
182   - sb << ' value="${' << domainInstance << '.' << property.name << '}"'
183   - sb << '/>'
184   - }
185   - sb as String
186   - }
187   - }
188   -
189   - private renderBooleanEditor(domainClass, property) {
190   - if (!cp) {
191   - return "<g:checkBox name=\"${property.name}\" value=\"\${${domainInstance}?.${property.name}}\" />"
192   - } else {
193   - def sb = new StringBuilder("<g:checkBox name=\"${property.name}\" ")
194   - if (cp.widget) sb << "widget=\"${cp.widget}\" ";
195   - cp.attributes.each { k, v ->
196   - sb << "${k}=\"${v}\" "
197   - }
198   - sb << "value=\"\${${domainInstance}?.${property.name}}\" />"
199   - return sb.toString()
200   - }
201   - }
202   -
203   - private renderDateEditor(domainClass, property) {
204   - def precision = (property.type == Date || property.type == java.sql.Date || property.type == Calendar) ? "day" : "minute";
205   - if (!cp) {
206   - return "<g:datePicker name=\"${property.name}\" precision=\"${precision}\" value=\"\${${domainInstance}?.${property.name}}\" />"
207   - } else {
208   - if (!cp.editable) {
209   - return "\${${domainInstance}?.${property.name}?.toString()}"
210   - } else {
211   - def sb = new StringBuilder("<g:datePicker name=\"${property.name}\" ")
212   - if (cp.format) sb << "format=\"${cp.format}\" "
213   - if (cp.widget) sb << "widget=\"${cp.widget}\" "
214   - cp.attributes.each { k, v ->
215   - sb << "${k}=\"${v}\" "
216   - }
217   - if (!cp.attributes.precision){
218   - sb << "precision=\"${precision}\" "
219   - }
220   - sb << " value=\"\${${domainInstance}?.${property.name}}\" ${renderNoSelection(property)} />"
221   -
222   - return sb.toString()
223   - }
224   - }
225   - }
226   -
227   - private renderSelectTypeEditor(type, domainClass,property) {
228   - if (!cp) {
229   - return "<g:${type}Select name=\"${property.name}\" value=\"\${${domainInstance}?.${property.name}}\" />"
230   - } else {
231   - def sb = new StringBuilder("<g:${type}Select name=\"${property.name}\" ")
232   - if (cp.widget) sb << "widget=\"${cp.widget}\" ";
233   - cp.attributes.each { k, v ->
234   - sb << "${k}=\"${v}\" "
235   - }
236   - sb << "value=\"\${${domainInstance}?.${property.name}}\" ${renderNoSelection(property)} />"
237   - return sb.toString()
238   - }
239   - }
240   -
241   - private renderNoSelection(property) {
242   - if (isOptional()) {
243   - if (property.manyToOne || property.oneToOne) {
244   - return " noSelection=\"['null': '']\""
245   - } else if (property.type == Date || property.type == java.sql.Date || property.type == java.sql.Time || property.type == Calendar) {
246   - return "default=\"none\" noSelection=\"['': '']\""
247   - } else {
248   - return " noSelection=\"['': '']\""
249   - }
250   - }
251   - return ""
252   - }
253   -
254   - private boolean isRequired() {
255   - !isOptional()
256   - }
257   -
258   - private boolean isOptional() {
259   - if(!cp) {
260   - return false
261   - }
262   - else {
263   - cp.nullable || (cp.propertyType == String && cp.blank) || cp.propertyType in [boolean, Boolean]
264   - }
265   - }
266   -%>
61 src/templates/scaffolding/show.gsp
... ... @@ -1,61 +0,0 @@
1   -<% import grails.persistence.Event %>
2   -<%=packageName%>
3   -<!doctype html>
4   -<html>
5   - <head>
6   - <meta name="layout" content="main">
7   - <g:set var="entityName" value="\${message(code: '${domainClass.propertyName}.label', default: '${className}')}" />
8   - <title><g:message code="default.show.label" args="[entityName]" /></title>
9   - </head>
10   - <body>
11   - <a href="#show-${domainClass.propertyName}" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
12   - <div class="nav" role="navigation">
13   - <ul>
14   - <li><a class="home" href="\${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
15   - <li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
16   - <li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
17   - </ul>
18   - </div>
19   - <div id="show-${domainClass.propertyName}" class="content scaffold-show" role="main">
20   - <h1><g:message code="default.show.label" args="[entityName]" /></h1>
21   - <g:if test="\${flash.message}">
22   - <div class="message" role="status">\${flash.message}</div>
23   - </g:if>
24   - <ol class="property-list ${domainClass.propertyName}">
25   - <% excludedProps = Event.allEvents.toList() << 'id' << 'version'
26   - allowedNames = domainClass.persistentProperties*.name << 'dateCreated' << 'lastUpdated'
27   - props = domainClass.properties.findAll { allowedNames.contains(it.name) && !excludedProps.contains(it.name) }
28   - Collections.sort(props, comparator.constructors[0].newInstance([domainClass] as Object[]))
29   - props.each { p -> %>
30   - <g:if test="\${${propertyName}?.${p.name}}">
31   - <li class="fieldcontain">
32   - <span id="${p.name}-label" class="property-label"><g:message code="${domainClass.propertyName}.${p.name}.label" default="${p.naturalName}" /></span>
33   - <% if (p.isEnum()) { %>
34   - <span class="property-value" aria-labelledby="${p.name}-label"><g:fieldValue bean="\${${propertyName}}" field="${p.name}"/></span>
35   - <% } else if (p.oneToMany || p.manyToMany) { %>
36   - <g:each in="\${${propertyName}.${p.name}}" var="${p.name[0]}">
37   - <span class="property-value" aria-labelledby="${p.name}-label"><g:link controller="${p.referencedDomainClass?.propertyName}" action="show" id="\${${p.name[0]}.id}">\${${p.name[0]}?.encodeAsHTML()}</g:link></span>
38   - </g:each>
39   - <% } else if (p.manyToOne || p.oneToOne) { %>
40   - <span class="property-value" aria-labelledby="${p.name}-label"><g:link controller="${p.referencedDomainClass?.propertyName}" action="show" id="\${${propertyName}?.${p.name}?.id}">\${${propertyName}?.${p.name}?.encodeAsHTML()}</g:link></span>
41   - <% } else if (p.type == Boolean || p.type == boolean) { %>
42   - <span class="property-value" aria-labelledby="${p.name}-label"><g:formatBoolean boolean="\${${propertyName}?.${p.name}}" /></span>
43   - <% } else if (p.type == Date || p.type == java.sql.Date || p.type == java.sql.Time || p.type == Calendar) { %>
44   - <span class="property-value" aria-labelledby="${p.name}-label"><g:formatDate date="\${${propertyName}?.${p.name}}" /></span>
45   - <% } else if(!p.type.isArray()) { %>
46   - <span class="property-value" aria-labelledby="${p.name}-label"><g:fieldValue bean="\${${propertyName}}" field="${p.name}"/></span>
47   - <% } %>
48   - </li>
49   - </g:if>
50   - <% } %>
51   - </ol>
52   - <g:form>
53   - <fieldset class="buttons">
54   - <g:hiddenField name="id" value="\${${propertyName}?.id}" />
55   - <g:link class="edit" action="edit" id="\${${propertyName}?.id}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
56   - <g:actionSubmit class="delete" action="delete" value="\${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('\${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
57   - </fieldset>
58   - </g:form>
59   - </div>
60   - </body>
61   -</html>
37 test/unit/grails/plugin/emberscaffolding/ScaffoldingHelperSpec.groovy
... ... @@ -0,0 +1,37 @@
  1 +package grails.plugin.emberscaffolding
  2 +
  3 +import grails.plugin.emberscaffolding.test.Pirate
  4 +import grails.test.mixin.Mock
  5 +import spock.lang.Specification
  6 +
  7 +@Mock(Pirate)
  8 +class ScaffoldingHelperSpec extends Specification {
  9 +
  10 + void 'can get list of bindable properties for a domain class'() {
  11 + expect:
  12 + ScaffoldingHelper.getBindableProperties(Pirate) == ['name', 'nickname', 'dateOfBirth']
  13 + }
  14 +
  15 + void 'can get property values for a domain instance'() {
  16 + given:
  17 + def pirate = new Pirate(name: 'Edward Teach', nickname: 'Blackbeard')
  18 +
  19 + expect:
  20 + ScaffoldingHelper.getBindablePropertyValues(pirate) == [name: pirate.name, nickname: pirate.nickname, dateOfBirth: pirate.dateOfBirth]
  21 + }
  22 +
  23 + void 'can get property values for a collection of domain instances'() {
  24 + given:
  25 + def pirates = [
  26 + new Pirate(name: 'Edward Teach', nickname: 'Blackbeard'),
  27 + new Pirate(name: 'Jack Rackham', nickname: 'Calico Jack', dateOfBirth: new Date(1682, 12, 21))
  28 + ]
  29 +
  30 + expect:
  31 + ScaffoldingHelper.getBindablePropertyValues(pirates) == [
  32 + [name: pirates[0].name, nickname: pirates[0].nickname, dateOfBirth: pirates[0].dateOfBirth],
  33 + [name: pirates[1].name, nickname: pirates[1].nickname, dateOfBirth: pirates[1].dateOfBirth]
  34 + ]
  35 + }
  36 +
  37 +}
572 web-app/WEB-INF/tld/c.tld
... ... @@ -0,0 +1,572 @@
  1 +<?xml version="1.0" encoding="UTF-8" ?>
  2 +
  3 +<taglib xmlns="http://java.sun.com/xml/ns/javaee"
  4 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5 + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
  6 + version="2.1">
  7 +
  8 + <description>JSTL 1.2 core library</description>
  9 + <display-name>JSTL core</display-name>
  10 + <tlib-version>1.2</tlib-version>
  11 + <short-name>c</short-name>
  12 + <uri>http://java.sun.com/jsp/jstl/core</uri>
  13 +
  14 + <validator>
  15 + <description>
  16 + Provides core validation features for JSTL tags.
  17 + </description>
  18 + <validator-class>
  19 + org.apache.taglibs.standard.tlv.JstlCoreTLV
  20 + </validator-class>
  21 + </validator>
  22 +
  23 + <tag>
  24 + <description>
  25 + Catches any Throwable that occurs in its body and optionally
  26 + exposes it.
  27 + </description>
  28 + <name>catch</name>
  29 + <tag-class>org.apache.taglibs.standard.tag.common.core.CatchTag</tag-class>
  30 + <body-content>JSP</body-content>
  31 + <attribute>
  32 + <description>
  33 +Name of the exported scoped variable for the
  34 +exception thrown from a nested action. The type of the
  35 +scoped variable is the type of the exception thrown.
  36 + </description>
  37 + <name>var</name>
  38 + <required>false</required>
  39 + <rtexprvalue>false</rtexprvalue>
  40 + </attribute>
  41 + </tag>
  42 +
  43 + <tag>
  44 + <description>
  45 + Simple conditional tag that establishes a context for
  46 + mutually exclusive conditional operations, marked by
  47 + &lt;when&gt; and &lt;otherwise&gt;
  48 + </description>
  49 + <name>choose</name>
  50 + <tag-class>org.apache.taglibs.standard.tag.common.core.ChooseTag</tag-class>
  51 + <body-content>JSP</body-content>
  52 + </tag>
  53 +
  54 + <tag>
  55 + <description>
  56 + Simple conditional tag, which evalutes its body if the
  57 + supplied condition is true and optionally exposes a Boolean
  58 + scripting variable representing the evaluation of this condition
  59 + </description>
  60 + <name>if</name>
  61 + <tag-class>org.apache.taglibs.standard.tag.rt.core.IfTag</tag-class>
  62 + <body-content>JSP</body-content>