Scaffolding a Generic Admin - Part 3 - Illudium Code Generator Template
If you are not using Illudium to generate your application classes, I encourage you to check it out. Out of the box, the code generator comes with several templates for generating CFCs, Transfer and ColdSpring XML, Actionscript VOs (value objects) and TOs (simple transfer objects designed for Flex development). Modifying, or creating your own templates is encouraged and easy. The template I developed for this series uses ColdFusion templating, but you can also use XSLT or a combination of both...
Illudium connects to the ColdFusion administrator, so you will need to enter your CF administrator password when you launch or reload Illudium. When you pass the authentication stage, Illudium prepares a list of datasource names that you can choose to select the tables that will form the basis for your generated code.
For this post I created a MySQL database named "triviagame" and ran the MySQL script from Part 2 to create the tables. After creating the database and tables, I also created a datasource name called "triviagame".
To view and/or save the generated code you will need to unpack the enclosed archive into the folder /xsl/projects/ found under the Illudium web root folder. After extracting the files, you will be able to choose the the "transfer-dispatcher-cfm" template from the list of available templates.
If you quickly compare the files generated from this template with the default template, you'll notice that it is purposefully lean. Transfer ORM makes this possible. I'm using Transfer for all CRUD operations (in place of a DAO) and list queries (in place of a "Table Data" Gateway). This allows me to focus on the controller, service and decorator CFCs. Let's take a look at some example code generated from the "trivia_game" table.
Below is the generated code for GameService.cfc.
<cffunction name="init" access="public" output="false" returntype="model.game.GameService">
<cfargument name="transfer" type="transfer.com.Transfer" required="true" />
<cfargument name="TransferConfig" type="config.TransferConfig" required="true" />
<cfset variables.transfer = arguments.transfer />
<cfset variables.TransferConfig = arguments.TransferConfig />
<cfreturn this/>
</cffunction>
<cffunction name="getGame" access="public" output="false" returntype="any">
<cfargument name="Id" type="Numeric" required="true" />
<cfset var Game = variables.transfer.get("Game.Game",arguments.Id) />
<cfset Game.setTransferMetadata(variables.transfer.getTransferMetadata("Game.Game")) />
<cfset Game.setParents(variables.TransferConfig.getParents("Game.Game")) />
<cfreturn Game />
</cffunction>
<cffunction name="getGames" access="public" output="false" returntype="query">
<cfargument name="Id" type="Numeric" required="false" />
<cfargument name="UserId" type="Numeric" required="false" />
<cfargument name="ThemeId" type="Boolean" required="false" />
<cfargument name="Name" type="String" required="false" />
<cfargument name="Description" type="String" required="false" />
<cfargument name="Active" type="Boolean" required="false" />
<cfargument name="CreatedDate" type="Date" required="false" />
<cfargument name="orderby" type="string" required="false" />
<cfset var qList = "" />
<cfset var key = "" />
<cfset var propertyMap = StructNew()>
<cfloop item="key" collection="#arguments#">
<cfif Len(arguments[#key#])>
<cfset StructInsert(propertyMap,#key#,arguments[#key#])>
</cfif>
</cfloop>
<cfif StructIsEmpty(propertyMap)>
<cfset qList = variables.transfer.list("Game.Game") />
<cfelse>
<cfset qList = variables.transfer.listByPropertyMap("Game.Game",propertyMap) />
</cfif>
<cfreturn qList />
</cffunction>
<cffunction name="new" access="public" output="false" returntype="any">
<cfset var Game = variables.transfer.new("Game.Game") />
<cfset Game.setTransferMetadata(variables.transfer.getTransferMetadata("Game.Game")) />
<cfset Game.setParents(variables.TransferConfig.getParents("Game.Game")) />
<cfreturn Game />
</cffunction>
<cffunction name="saveGame" access="public" output="false" returntype="any">
<cfargument name="Id" type="Numeric" required="true" />
<cfargument name="UserId" type="Numeric" required="false" />
<cfargument name="ThemeId" type="Boolean" required="false" />
<cfargument name="Name" type="String" required="false" />
<cfargument name="Description" type="String" required="false" />
<cfargument name="Active" type="Boolean" required="false" />
<cfargument name="CreatedDate" type="Date" required="false" />
<cfset var Game = variables.transfer.get("Game.Game" ,arguments.Id) />
<cfset Game.setId(arguments.Id) />
<cfset Game.setParentUser(variables.transfer.get("User.User",arguments.UserId)) />
<cfset Game.setParentTheme(variables.transfer.get("Theme.Theme",arguments.ThemeId)) />
<cfset Game.setName(arguments.Name) />
<cfset Game.setDescription(arguments.Description) />
<cfset Game.setActive(arguments.Active) />
<cfset Game.setCreatedDate(arguments.CreatedDate) />
<cfset variables.transfer.save(Game) />
<cfset Game.setTransferMetadata(variables.transfer.getTransferMetadata("Game.Game")) />
<cfset Game.setParents(variables.TransferConfig.getParents("Game.Game")) />
<cfreturn Game />
</cffunction>
<cffunction name="deleteGame" access="public" output="false" returntype="void">
<cfargument name="Id" type="Numeric" required="true" />
<cfset var Game = variables.transfer.get("Game.Game",arguments.Id) />
<cfset variables.transfer.delete(Game) />
</cffunction>
</cfcomponent>
For methods that return a Game, you'll notice that I invoke a couple of methods, setTransferMetaData() and setParents(), prior to returning the object. These methods are designed to make the TransferObject more "self-describing" in support of the generic "Form", "Bean" and "List" views that make up the generic admin. We'll revisit these later in the series.
The other portion of code to note it the "setXXX" methods invoked in saveGame(). Here lies the justification for my PK/FK naming convention. A TransferObject must be fully composed prior to being saved. In this case, a Game has a parent User and Theme, but the arguments for saveGame() are simple form variables (UserId and ThemeId) passed to it from the Game controller. I've set up the template to check for any attribute containing "Id" in the name, so that the proper compostion method can be called. For example, we need to use
instead of
Did I mention that I'm happy Brian added CFM templating to Illudium? Pulling that type of manipulation off in XSL would have been a big headache for me!
Next we'll look at the generated code for GameController.cfc.
<cffunction name="init" access="public" output="false" returntype="listener.GameController">
<cfargument name="GameService" type="model.game.GameService" required="true" />
<cfset super.init() />
<cfset variables.GameService = arguments.GameService />
<cfreturn this />
</cffunction>
<cffunction name="onRequest" access="public" output="true" returntype="void">
<cfargument name="event" type="com.fancybread.event.RequestEvent" required="true">
<!--- determine command and process action --->
<cfswitch expression="#arguments.event.getRequestObject()#">
<cfcase value="Game">
<cfswitch expression="#arguments.event.getRequestAction()#">
<cfcase value="delete">
<cfset deleteGame(arguments.event) />
<cfset getGameList(arguments.event) />
</cfcase>
<cfcase value="edit,view">
<cfset getGame(arguments.event) />
</cfcase>
<cfcase value="list">
<cfset getGameList(arguments.event) />
</cfcase>
<cfcase value="new">
<cfset newGame(arguments.event) />
</cfcase>
<cfcase value="save">
<cfset saveGame(arguments.event) />
</cfcase>
</cfswitch>
</cfcase>
</cfswitch>
</cffunction>
<cffunction name="deleteGame" access="private" output="false" returntype="void">
<cfargument name="event" type="com.fancybread.event.RequestEvent" required="true">
<cfset variables.GameService.deleteGame(arguments.event.getUrl("id")) />
</cffunction>
<cffunction name="getGame" access="private" output="false" returntype="void">
<cfargument name="event" type="com.fancybread.event.RequestEvent" required="true">
<cfset var Game = variables.GameService.getGame(arguments.event.getUrl("id")) />
<cfset arguments.event.addObject("Game",Game)>
</cffunction>
<cffunction name="getGameList" access="private" output="false" returntype="void">
<cfargument name="event" type="com.fancybread.event.RequestEvent" required="true">
<cfset var GameList = variables.GameService.getGames() />
<cfset arguments.event.addQuery("GameList",GameList)>
</cffunction>
<cffunction name="newGame" access="private" output="false" returntype="void">
<cfargument name="event" type="com.fancybread.event.RequestEvent" required="true">
<cfset var Game = variables.GameService.new() />
<cfset arguments.event.addObject("Game",Game)>
</cffunction>
<cffunction name="saveGame" access="private" output="false" returntype="void">
<cfargument name="event" type="com.fancybread.event.RequestEvent" required="true">
<cfset var Game = variables.GameService.saveGame(argumentCollection=arguments.event.getCollection("form")) />
<cfset arguments.event.addObject("Game",Game)>
</cffunction>
</cfcomponent>
The controller CFCs generated from this template extend "com.fancybread.event.RequestListener" which is part of the Dispatcher event pacakge. A RequestListener has an onRequest() method which receives a generic event object. I will explain the inner workings of a Dispatcher application in more detail in my next post. For now, all we need to know is that there is a controller for each business object and that the onRequest() method is invoked on all controllers for each page request. Whether the controller takes action based on the event is decided by the logic within onRequest().
Next we have the TransferObject Decorator.
<cffunction name="addError" access="private" returntype="void" output="false">
<cfargument name="error" type="struct" required="true">
<cfset var errors = ArrayNew(1) />
<cfif StructKeyExists(variables,"errors")>
<cfset errors = variables.errors />
</cfif>
<cfset ArrayAppend(variables.errors,error) />
</cffunction>
<cffunction name="getErrors" access="public" returntype="array" output="false">
<cfset var errors = ArrayNew(1) />
<cfif StructKeyExists(variables,"errors")>
<cfset errors = variables.errors />
</cfif>
<cfreturn variables.errors />
</cffunction>
<cffunction name="isValidForSave" access="public" returntype="array" output="false">
<cfset var thisError = structNew() />
<!--- Id --->
<cfif (len(trim(getId())) AND NOT isNumeric(trim(getId())))>
<cfset thisError.field = "Id" />
<cfset thisError.type = "invalidType" />
<cfset thisError.message = "Id is not numeric" />
<cfset addError(duplicate(thisError)) />
</cfif>
<!--- UserId --->
<cfif (NOT len(trim(getUserId())))>
<cfset thisError.field = "UserId" />
<cfset thisError.type = "required" />
<cfset thisError.message = "UserId is required" />
<cfset addError(duplicate(thisError)) />
</cfif>
<cfif (len(trim(getUserId())) AND NOT isNumeric(trim(getUserId())))>
<cfset thisError.field = "UserId" />
<cfset thisError.type = "invalidType" />
<cfset thisError.message = "UserId is not numeric" />
<cfset addError(duplicate(thisError)) />
</cfif>
<!--- ThemeId --->
<cfif (NOT len(trim(getThemeId())))>
<cfset thisError.field = "ThemeId" />
<cfset thisError.type = "required" />
<cfset thisError.message = "ThemeId is required" />
<cfset addError(duplicate(thisError)) />
</cfif>
<cfif (len(trim(getThemeId())) AND NOT isBoolean(trim(getThemeId())))>
<cfset thisError.field = "ThemeId" />
<cfset thisError.type = "invalidType" />
<cfset thisError.message = "ThemeId is not boolean" />
<cfset addError(duplicate(thisError)) />
</cfif>
<!--- Name --->
<cfif (NOT len(trim(getName())))>
<cfset thisError.field = "Name" />
<cfset thisError.type = "required" />
<cfset thisError.message = "Name is required" />
<cfset addError(duplicate(thisError)) />
</cfif>
<cfif (len(trim(getName())) AND NOT IsSimpleValue(trim(getName())))>
<cfset thisError.field = "Name" />
<cfset thisError.type = "invalidType" />
<cfset thisError.message = "Name is not a string" />
<cfset addError(duplicate(thisError)) />
</cfif>
<cfif (len(trim(getName())) GT 50)>
<cfset thisError.field = "Name" />
<cfset thisError.type = "tooLong" />
<cfset thisError.message = "Name is too long" />
<cfset addError(duplicate(thisError)) />
</cfif>
<!--- Description --->
<cfif (len(trim(getDescription())) AND NOT IsSimpleValue(trim(getDescription())))>
<cfset thisError.field = "Description" />
<cfset thisError.type = "invalidType" />
<cfset thisError.message = "Description is not a string" />
<cfset addError(duplicate(thisError)) />
</cfif>
<cfif (len(trim(getDescription())) GT 65535)>
<cfset thisError.field = "Description" />
<cfset thisError.type = "tooLong" />
<cfset thisError.message = "Description is too long" />
<cfset addError(duplicate(thisError)) />
</cfif>
<!--- Active --->
<cfif (NOT len(trim(getActive())))>
<cfset thisError.field = "Active" />
<cfset thisError.type = "required" />
<cfset thisError.message = "Active is required" />
<cfset addError(duplicate(thisError)) />
</cfif>
<cfif (len(trim(getActive())) AND NOT isBoolean(trim(getActive())))>
<cfset thisError.field = "Active" />
<cfset thisError.type = "invalidType" />
<cfset thisError.message = "Active is not boolean" />
<cfset addError(duplicate(thisError)) />
</cfif>
<!--- CreatedDate --->
<cfif (NOT len(trim(getCreatedDate())))>
<cfset thisError.field = "CreatedDate" />
<cfset thisError.type = "required" />
<cfset thisError.message = "CreatedDate is required" />
<cfset addError(duplicate(thisError)) />
</cfif>
<cfif (len(trim(getCreatedDate())) AND NOT isDate(trim(getCreatedDate())))>
<cfset thisError.field = "CreatedDate" />
<cfset thisError.type = "invalidType" />
<cfset thisError.message = "CreatedDate is not a date" />
<cfset addError(duplicate(thisError)) />
</cfif>
<cfif getErrors()>
<cfreturn false />
<cfelse>
<cfreturn true/>
</cfif>
</cffunction>
<cffunction name="setTransferMetadata" access="public" returntype="void" output="false">
<cfargument name="metadata" type="transfer.com.object.Object" required="true">
<cfset variables.metadata = arguments.metadata />
</cffunction>
<cffunction name="getTransferMetadata" access="public" returntype="struct" output="false">
<cfreturn variables.metadata />
</cffunction>
<cffunction name="setParents" access="public" returntype="any" output="false">
<cfargument name="parentArray" type="array" required="true">
<cfset variables.parentArray = arguments.parentArray />
</cffunction>
<cffunction name="getParents" access="public" returntype="any" output="false">
<cfreturn variables.parentArray />
</cffunction>
<cffunction name="getProperty" access="public" returntype="any" output="false">
<cfargument name="name" type="string" required="true">
<cfreturn Evaluate("get#arguments.name#()") />
</cffunction>
</cfcomponent>
Being able to decorate a TransferObject gives you the flexibility harness the power of Transfer while adding methods that you might like to use for business objects. In addition to the methods that enhance the "self-describing" nature of a TransferObject, I have added methods that add support for validation (isValidForSave(), addError() and getErrors()) and a generic getProperty() method.
The two other parts of the template create code snippets to be cut and pasted into transfer.xml object definitions
<id name="Id" type="Numeric" />
<property name="UserId" type="Numeric" column="UserId" />
<property name="ThemeId" type="Boolean" column="ThemeId" />
<property name="Name" type="String" column="Name" />
<property name="Description" type="String" column="Description" />
<property name="Active" type="Boolean" column="Active" />
<property name="CreatedDate" type="Date" column="CreatedDate" />
</object>
and ColdSpring bean definitions xml.
<constructor-arg name="transfer">
<ref bean="transfer"/>
</constructor-arg>
<constructor-arg name="TransferConfig">
<ref bean="TransferConfig"/>
</constructor-arg>
</bean>
<bean id="GameController" class="listener.GameController">
<constructor-arg name="GameService">
<ref bean="GameService"/>
</constructor-arg>
</bean>
There's a fair bit to digest here and it may not make a lot of sense at present. It's my hope to provide some clarity to the picture when I continue with the following post(s) on how to set up a Dispatcher application.



There are no comments for this entry.
[Add Comment]