| /* |
| * Copyright 2004-2005 the original author or authors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.codehaus.groovy.grails.scaffolding; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.codehaus.groovy.grails.commons.GrailsDomainClass; |
| import org.codehaus.groovy.grails.commons.GrailsApplication; |
| import org.codehaus.groovy.grails.scaffolding.GrailsTemplateGenerator; |
| import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU; |
| /** |
| * Default implementation of the generator that generates grails artifacts (controllers, views etc.) |
| * from the domain model |
| * |
| * @author Graeme Rocher |
| * @since 09-Feb-2006 |
| */ |
| class DefaultGrailsTemplateGenerator implements GrailsTemplateGenerator { |
| |
| Log LOG = LogFactory.getLog(DefaultGrailsTemplateGenerator.class); |
| @Property String basedir |
| @Property boolean overwrite = false |
| def engine = new groovy.text.SimpleTemplateEngine() |
| |
| // a closure that uses the type to render the appropriate editor |
| def renderEditor = { property -> |
| def domainClass = property.domainClass |
| def cp = domainClass.constrainedProperties[property.name] |
| |
| def display = (cp ? cp.display : true) |
| if(!display) return '' |
| |
| def buf = new StringBuffer("<tr class='prop'>") |
| buf << "<td valign='top' style='text-align:left;' width='20%'><label for='${property.name}'>${property.naturalName}:</label></td>" |
| buf << "<td valign='top' style='text-align:left;' width='80%' class='\${hasErrors(bean:${domainClass.propertyName},field:'${property.name}','errors')}'>" |
| if(Number.class.isAssignableFrom(property.type)) |
| buf << renderNumberEditor(domainClass,property) |
| else if(property.type == String.class) |
| buf << renderStringEditor(domainClass,property) |
| else if(property.type == Boolean.class || property.type == boolean.class) |
| buf << renderBooleanEditor(domainClass,property) |
| else if(property.type == Date.class) |
| buf << renderDateEditor(domainClass,property) |
| else if(property.type == TimeZone.class) |
| buf << renderSelectTypeEditor("timeZone",domainClass,property) |
| else if(property.type == Locale.class) |
| buf << renderSelectTypeEditor("locale",domainClass,property) |
| else if(property.type == Currency.class) |
| buf << renderSelectTypeEditor("currency",domainClass,property) |
| else if(property.type==([] as Byte[]).class) //TODO: Bug in groovy means i have to do this :( |
| buf << renderByteArrayEditor(domainClass,property) |
| else if(property.manyToOne || property.oneToOne) |
| buf << renderManyToOne(domainClass,property) |
| else if(property.oneToMany || property.manyToMany) |
| buf << renderOneToMany(domainClass,property) |
| |
| buf << '</td></tr>' |
| return buf.toString() |
| } |
| |
| public void generateViews(GrailsDomainClass domainClass, String destdir) { |
| if(!destdir) |
| throw new IllegalArgumentException("Argument [destdir] not specified") |
| |
| def viewsDir = new File("${destdir}/grails-app/views/${domainClass.propertyName}") |
| if(!viewsDir.exists()) |
| viewsDir.mkdirs() |
| |
| LOG.info("Generating list view for domain class [${domainClass.fullName}]") |
| generateListView(domainClass,viewsDir) |
| LOG.info("Generating show view for domain class [${domainClass.fullName}]") |
| generateShowView(domainClass,viewsDir) |
| LOG.info("Generating edit view for domain class [${domainClass.fullName}]") |
| generateEditView(domainClass,viewsDir) |
| LOG.info("Generating create view for domain class [${domainClass.fullName}]") |
| generateCreateView(domainClass,viewsDir) |
| } |
| |
| public void generateController(GrailsDomainClass domainClass, String destdir) { |
| if(!destdir) |
| throw new IllegalArgumentException("Argument [destdir] not specified") |
| |
| if(domainClass) { |
| def destFile = new File("${destdir}/grails-app/controllers/${domainClass.shortName}Controller.groovy") |
| if(destFile.exists()) { |
| LOG.info("Controller ${destFile.name} already exists skipping") |
| return |
| } |
| destFile.parentFile.mkdirs() |
| |
| def templateText = ''' |
| class ${className}Controller { |
| @Property index = { redirect(action:list,params:params) } |
| |
| @Property list = { |
| if(!params['max']) params['max'] = 10 |
| [ ${propertyName}List: ${className}.list( params ) ] |
| } |
| |
| @Property show = { |
| [ ${propertyName} : ${className}.get( params['id'] ) ] |
| } |
| |
| @Property delete = { |
| def ${propertyName} = ${className}.get( params['id'] ) |
| if(${propertyName}) { |
| ${propertyName}.delete() |
| flash['message'] = "${className} \\${params['id']} deleted." |
| redirect(action:list) |
| } |
| else { |
| flash['message'] = "${className} not found with id \\${params['id']}" |
| redirect(action:list) |
| } |
| } |
| |
| @Property edit = { |
| def ${propertyName} = ${className}.get( params['id'] ) |
| |
| if(!${propertyName}) { |
| flash['message'] = "${className} not found with id \\${params['id']}" |
| redirect(action:list) |
| } |
| else { |
| return [ ${propertyName} : ${propertyName} ] |
| } |
| } |
| |
| @Property update = { |
| def ${propertyName} = ${className}.get( params['id'] ) |
| if(${propertyName}) { |
| ${propertyName}.properties = params |
| if(${propertyName}.save()) { |
| redirect(action:show,id:${propertyName}.id) |
| } |
| else { |
| render(view:'edit',model:[${propertyName}:${propertyName}]) |
| } |
| } |
| else { |
| flash['message'] = "${className} not found with id \\${params['id']}" |
| redirect(action:edit,id:params['id']) |
| } |
| } |
| |
| @Property create = { |
| def ${propertyName} = new ${className}() |
| ${propertyName}.properties = params |
| return ['${propertyName}':${propertyName}] |
| } |
| |
| @Property save = { |
| def ${propertyName} = new ${className}() |
| ${propertyName}.properties = params |
| if(${propertyName}.save()) { |
| redirect(action:show,id:${propertyName}.id) |
| } |
| else { |
| render(view:'create',model:[${propertyName}:${propertyName}]) |
| } |
| } |
| |
| }''' |
| |
| def binding = [ className: domainClass.shortName, propertyName:domainClass.propertyName ] |
| def t = engine.createTemplate(templateText) |
| |
| destFile.withWriter { w -> |
| t.make(binding).writeTo(w) |
| } |
| |
| LOG.info("Controller generated at ${destFile}") |
| } |
| } |
| |
| private renderStringEditor(domainClass, property) { |
| def cp = domainClass.constrainedProperties[property.name] |
| if(!cp) { |
| return "<input type='text' name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}' />" |
| } |
| else { |
| if(cp.maxLength > 250 && !cp.password && !cp.inList) { |
| return "<textarea rows='1' cols='1' name='${property.name}'>\${${domainClass.propertyName}?.${property.name}}</textarea>" |
| } |
| else { |
| if(cp.inList) { |
| def sb = new StringBuffer('<select ') |
| sb << "name='${property.name}'>"> |
| cp.inList.each { |
| sb << "<option value='${it}'>${it}</option>" |
| } |
| sb << '</select>' |
| return sb.toString() |
| } |
| else { |
| def sb = new StringBuffer('<input ') |
| cp.password ? sb << 'type="password" ' : sb << 'type="text" ' |
| if(!cp.editable) sb << 'readonly="readonly" ' |
| if(cp.maxLength < Integer.MAX_VALUE ) sb << "maxlength='${cp.maxLength}' " |
| sb << "name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></input>" |
| return sb.toString() |
| } |
| } |
| } |
| } |
| |
| private renderByteArrayEditor(domainClass,property) { |
| return "<input type='file' name='${property.name}'></input>" |
| } |
| |
| private renderManyToOne(domainClass,property) { |
| if(property.association) { |
| return "<g:select optionKey=\"id\" from=\"\${${property.type.name}.list()}\" name='${property.name}.id' value='\${${domainClass.propertyName}?.${property.name}?.id}'></g:select>" |
| } |
| } |
| |
| private renderOneToMany(domainClass,property) { |
| def sw = new StringWriter() |
| def pw = new PrintWriter(sw) |
| pw.println '<ul>' |
| pw.println " <g:each var='${property.name[0]}' in='\${${domainClass.propertyName}.${property.name}}'>" |
| pw.println " <li><g:link controller='${property.referencedDomainClass.propertyName}' action='show' id='\${${property.name[0]}.id}'>\${${property.name[0]}}</g:link></li>" |
| pw.println " </g:each>" |
| pw.println "</ul>" |
| pw.println "<g:link controller='${property.referencedDomainClass.propertyName}' params='[\"${domainClass.propertyName}.id\":${domainClass.propertyName}?.id]' action='create'>Add ${property.referencedDomainClass.shortName}</g:link>" |
| return sw.toString() |
| } |
| |
| private renderNumberEditor(domainClass,property) { |
| def cp = domainClass.constrainedProperties[property.name] |
| if(!cp) { |
| if(property.type == Byte.class) { |
| return "<g:select from='\${-128..127}' name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></g:select>" |
| } |
| else { |
| return "<input type='text' name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></input>" |
| } |
| } |
| else { |
| if(cp.range) { |
| return "<g:select from='\${${cp.range.from}..${cp.range.to}}' name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></g:select>" |
| } |
| else if(cp.size) { |
| return "<g:select from='\${${cp.size.from}..${cp.size.to}}' name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></g:select>" |
| } |
| else { |
| return "<input type='text' name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></input>" |
| } |
| } |
| } |
| |
| private renderBooleanEditor(domainClass,property) { |
| |
| def cp = domainClass.constrainedProperties[property.name] |
| if(!cp) { |
| return "<g:checkBox name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></g:checkBox>" |
| } |
| else { |
| def buf = new StringBuffer('<g:checkBox ') |
| if(cp.widget) buf << "widget='${cp.widget}'"; |
| |
| buf << "name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}' " |
| cp.attributes.each { k,v -> |
| buf << "${k}=\"${v}\" " |
| } |
| buf << '></g:checkBox>' |
| return buf.toString() |
| } |
| |
| } |
| |
| private renderDateEditor(domainClass,property) { |
| def cp = domainClass.constrainedProperties[property.name] |
| if(!cp) { |
| return "<g:datePicker name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></g:datePicker>" |
| } |
| else { |
| def buf = new StringBuffer('<g:datePicker ') |
| if(cp.widget) buf << "widget='${cp.widget}' "; |
| |
| if(cp.format) buf << "format='${cp.format}' "; |
| cp.attributes.each { k,v -> |
| buf << "${k}=\"${v}\" " |
| } |
| buf << "name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></g:datePicker>" |
| return buf.toString() |
| } |
| } |
| |
| private renderSelectTypeEditor(type,domainClass,property) { |
| def cp = domainClass.constrainedProperties[property.name] |
| if(!cp) { |
| return "<g:${type}Select name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></g:${type}Select>" |
| } |
| else { |
| def buf = new StringBuffer('<g:${type}Select ') |
| if(cp.widget) buf << "widget='${cp.widget}' "; |
| cp.attributes.each { k,v -> |
| buf << "${k}=\"${v}\" " |
| } |
| buf << "name='${property.name}' value='\${${domainClass.propertyName}?.${property.name}}'></g:${type}Select>" |
| return buf.toString() |
| } |
| } |
| |
| |
| |
| |
| private generateListView(domainClass, destDir) { |
| def listFile = new File("${destDir}/list.gsp") |
| if(!listFile.exists() || overwrite) { |
| def templateText = ''' |
| <html> |
| <head> |
| <title>${className} List</title> |
| <link rel="stylesheet" href="\\${createLinkTo(dir:'css',file:'main.css')}"></link> |
| </head> |
| <body> |
| <div class="nav"> |
| <span class="menuButton"><g:link action="index">Home</g:link></span> |
| <span class="menuButton"><g:link action="create">New ${className}</g:link></span> |
| </div> |
| <div class="body"> |
| <h1>${className} List</h1> |
| <g:if test="flash['message']"> |
| <div class="message"> |
| \\${flash['message']} |
| </div> |
| </g:if> |
| <table> |
| <tr> |
| <% |
| props = domainClass.properties.findAll { it.name != 'version' && it.type != Set.class } |
| Collections.sort(props, new org.codehaus.groovy.grails.scaffolding.DomainClassPropertyComparator(domainClass)) |
| %> |
| <%props.eachWithIndex { p,i -> |
| if(i < 6) {%> |
| <th>${p.naturalName}</th> |
| <%}}%> |
| <th></th> |
| </tr> |
| <g:each in="\\${${propertyName}List}"> |
| <tr> |
| <%props.eachWithIndex { p,i -> |
| if(i < 6) {%> |
| <td>\\${it.${p.name}}</td> |
| <%}}%> |
| <td class="actionButtons"> |
| <span class="actionButton"><g:link action="show" id="\\${it.id}">Show</g:link></span> |
| </td> |
| </tr> |
| </g:each> |
| </table> |
| </div> |
| </body> |
| </body> |
| ''' |
| |
| def t = engine.createTemplate(templateText) |
| def binding = [ domainClass: domainClass, className:domainClass.shortName,propertyName:domainClass.propertyName ] |
| |
| listFile.withWriter { w -> |
| t.make(binding).writeTo(w) |
| } |
| LOG.info("list view generated at ${listFile.absolutePath}") |
| } |
| } |
| |
| private generateShowView(domainClass,destDir) { |
| def showFile = new File("${destDir}/show.gsp") |
| if(!showFile.exists() || overwrite) { |
| def templateText = ''' |
| <html> |
| <head> |
| <title>Show ${className}</title> |
| <link rel="stylesheet" href="\\${createLinkTo(dir:'css',file:'main.css')}"></link> |
| </head> |
| <body> |
| <div class="nav"> |
| <span class="menuButton"><g:link action="index">Home</g:link></span> |
| <span class="menuButton"><g:link action="list">${className} List</g:link></span> |
| <span class="menuButton"><g:link action="create">New ${className}</g:link></span> |
| </div> |
| <div class="body"> |
| <h1>Show ${className}</h1> |
| <g:if test="\\${flash['message']}"> |
| <div class="message">\\${flash['message']}</div> |
| </g:if> |
| <div class="dialog"> |
| <table> |
| <% |
| props = domainClass.properties.findAll { it.name != 'version' } |
| Collections.sort(props, new org.codehaus.groovy.grails.scaffolding.DomainClassPropertyComparator(domainClass)) |
| %> |
| <%props.each { p ->%> |
| <tr class="prop"> |
| <td valign="top" style="text-align:left;" width="20%" class="name">${p.naturalName}:</td> |
| <% if(p.oneToMany) { %> |
| <td valign="top" style="text-align:left;" class="value"> |
| <ul> |
| <g:each var="${p.name[0]}" in="\\${${propertyName}.${p.name}}"> |
| <li><g:link controller="${p.referencedDomainClass?.propertyName}" action="show" id="\\${${p.name[0]}.id}">\\${${p.name[0]}}</g:link></li> |
| </g:each> |
| </ul> |
| </td> |
| <% } else if(p.manyToOne || p.oneToOne) { %> |
| <td valign="top" style="text-align:left;" class="value"><g:link controller="${p.referencedDomainClass?.propertyName}" action="show" id="\\${${propertyName}?.${p.name}?.id}">\\${${propertyName}?.${p.name}}</g:link></td> |
| <% } else { %> |
| <td valign="top" style="text-align:left;" class="value">\\${${propertyName}.${p.name}}</td> |
| <% } %> |
| </tr> |
| <%}%> |
| </table> |
| </div> |
| <div class="buttons"> |
| <g:form controller="${propertyName}"> |
| <input type="hidden" name="id" value="\\${${propertyName}?.id}" /> |
| <span class="button"><g:actionSubmit value="Edit" /></span> |
| <span class="button"><g:actionSubmit value="Delete" /></span> |
| </g:form> |
| </div> |
| </div> |
| </body> |
| </body> |
| ''' |
| |
| def t = engine.createTemplate(templateText) |
| def binding = [ domainClass: domainClass, className:domainClass.shortName,propertyName:domainClass.propertyName ] |
| |
| showFile.withWriter { w -> |
| t.make(binding).writeTo(w) |
| } |
| LOG.info("Show view generated at ${showFile.absolutePath}") |
| } |
| } |
| |
| private generateEditView(domainClass,destDir) { |
| def editFile = new File("${destDir}/edit.gsp") |
| if(!editFile.exists() || overwrite) { |
| def templateText = ''' |
| <html> |
| <head> |
| <title>Edit ${className}</title> |
| <link rel="stylesheet" href="\\${createLinkTo(dir:'css',file:'main.css')}"></link> |
| </head> |
| <body> |
| <div class="nav"> |
| <span class="menuButton"><g:link action="index">Home</g:link></span> |
| <span class="menuButton"><g:link action="list">${className} List</g:link></span> |
| <span class="menuButton"><g:link action="create">New ${className}</g:link></span> |
| </div> |
| <div class="body"> |
| <h1>Edit ${className}</h1> |
| <g:if test="\\${flash['message']}"> |
| <div class="message">\\${flash['message']}</div> |
| </g:if> |
| <g:hasErrors bean="\\${${propertyName}}"> |
| <div class="errors"> |
| <g:renderErrors bean="\\${${propertyName}}" as="list" /> |
| </div> |
| </g:hasErrors> |
| <div class="prop"> |
| <span class="name">Id:</span> |
| <span class="value">\\${${propertyName}?.id}</span> |
| <input type="hidden" name="${propertyName}.id" value="\\${${propertyName}?.id}" /> |
| </div> |
| <g:form controller="${propertyName}" method="post"> |
| <input type="hidden" name="id" value="\\${${propertyName}?.id}" /> |
| <div class="dialog"> |
| <table> |
| |
| <% |
| props = domainClass.properties.findAll { it.name != 'version' && it.name != 'id' } |
| Collections.sort(props, new org.codehaus.groovy.grails.scaffolding.DomainClassPropertyComparator(domainClass)) |
| %> |
| <%props.each { p ->%> |
| ${renderEditor(p)} |
| <%}%> |
| </table> |
| </div> |
| |
| <div class="buttons"> |
| <span class="button"><g:actionSubmit value="Update" /></span> |
| <span class="button"><g:actionSubmit value="Delete" /></span> |
| </div> |
| </g:form> |
| </div> |
| </body> |
| </body> |
| ''' |
| |
| def t = engine.createTemplate(templateText) |
| def binding = [ domainClass: domainClass, |
| className:domainClass.shortName, |
| propertyName:domainClass.propertyName, |
| renderEditor:renderEditor ] |
| |
| editFile.withWriter { w -> |
| t.make(binding).writeTo(w) |
| } |
| LOG.info("Edit view generated at ${editFile.absolutePath}") |
| } |
| } |
| |
| private generateCreateView(domainClass,destDir) { |
| def createFile = new File("${destDir}/create.gsp") |
| if(!createFile.exists() || overwrite) { |
| def templateText = ''' |
| <html> |
| <head> |
| <title>Create ${className}</title> |
| <link rel="stylesheet" href="\\${createLinkTo(dir:'css',file:'main.css')}"></link> |
| </head> |
| <body> |
| <div class="nav"> |
| <span class="menuButton"><g:link action="index">Home</g:link></span> |
| <span class="menuButton"><g:link action="list">${className} List</g:link></span> |
| </div> |
| <div class="body"> |
| <h1>Create ${className}</h1> |
| <g:if test="\\${flash['message']}"> |
| <div class="message">\\${flash['message']}</div> |
| </g:if> |
| <g:hasErrors bean="\\${${propertyName}}"> |
| <div class="errors"> |
| <g:renderErrors bean="\\${${propertyName}}" as="list" /> |
| </div> |
| </g:hasErrors> |
| <g:form action="save" method="post"> |
| <div class="dialog"> |
| <table> |
| |
| <% |
| props = domainClass.properties.findAll { it.name != 'version' && it.name != 'id' } |
| Collections.sort(props, new org.codehaus.groovy.grails.scaffolding.DomainClassPropertyComparator(domainClass)) |
| %> |
| <%props.each { p -> |
| if(p.type != Set.class) { %> |
| ${renderEditor(p)} |
| <%}}%> |
| </table> |
| </div> |
| <div class="buttons"> |
| <span class="formButton"> |
| <input type="submit" value="Create"></input> |
| </span> |
| </div> |
| </g:form> |
| </div> |
| </body> |
| </body> |
| ''' |
| |
| def t = engine.createTemplate(templateText) |
| def binding = [ domainClass: domainClass, |
| className:domainClass.shortName, |
| propertyName:domainClass.propertyName, |
| renderEditor:renderEditor ] |
| |
| createFile.withWriter { w -> |
| t.make(binding).writeTo(w) |
| } |
| LOG.info("Create view generated at ${createFile.absolutePath}") |
| } |
| } |
| } |