Build a Code Generator in Five Easy Steps

 
Jan 11, 2007

by Brian Rinaldi

Those of you who read my blog know that I've been developing an open-source code generator that uses database metadata to generate ColdFusion components and other code snippets. I wrote about an earlier version of this project in my December blog post ("Create Your Own Code Generator"). This version had a number of limitations, such as limited RDMS support, although its biggest drawback was the quality of the code it generated. In August, I released the Illudium PU-36 Code Generator, a more advanced version of this project that addressed these deficiencies and made it easier to tweak and customize the generated code.

In this article, I hope to demonstrate how the code generator was built. I covers a number of important techniques that can be useful for many other projects. Specifically, I discuss the five items you need to build your own code generator:

  1. the Admin API in ColdFusion MX 7;
  2. accessing database metadata in varying RDMS;
  3. converting this data to a basic XML format;
  4. transforming your XML into code with XSL;
  5. the CFML code necessary to bring the first four parts together into a functional application.

The current version of my generator is built with Flex using the Cairngorm Microarchitecture, but that is not central to understanding how it works, so I will limit my discussion to the ColdFusion code.

Step 1: Admin API

The Admin API is a documented means of accessing all of the ColdFusion Administrator's internal ColdFusion functions, such as adding a datasource or a mapping. This feature, introduced in ColdFusion MX 7, was previously available through the use of the ServiceFactory Java component which was undocumented and, therefore, unsupported (see http://coldfusion.sys-con.com/read/41652.htm). I chose to use the documented Admin API for my generator, which means it will only work in version 7. If anyone is so inclined, the changes required to make it work with 6.1 using the ServiceFactory should be minimal.

The first step in building this type of code generator is to get a list of the available datasources. The Admin API makes this extremely easy (see Listing 1).

Listing 1: Getting the List of Available Datasources
<cfcomponent name="adminAPIFacade" output="false" hint="a wrapper for admiapi functions">
<cffunction name="init" access="public" output="false" returntype="adminAPIFacade">
<cfargument name="administratorPassword" type="string" required="true">
<cfset variables.administrator = createObject("component","cfide.adminapi.administrator").login(arguments.administratorPassword)>
<cfset variables.datasource = createObject("component","cfide.adminapi.datasource")>
<cfset variables.arrDSNs = arrayNew(1)>
<cfset setDatasources()>
<cfreturn this>
</cffunction>

<cffunction name="setDatasources" access="public" output="false" returntype="void">
<cfset var dsns = variables.datasource.getdatasources()>
<cfset var objDatasource = "">
<cfset var thisDSN = "">
<cfset var thisType = "">
<cfloop collection="#dsns#" item="thisDSN">
<cfset thisType = driverOrClassToType(dsns[thisDSN])>
<cfif len(thisType)>
<cfset objDatasource = createObject("component","cfcgenerator.com.cf.model.datasource.datasource").init(dsns[thisDSN].name,thisType)>
<cfset arrayAppend(variables.arrDSNs,objDatasource)>
</cfif>
</cfloop>
</cffunction>

<cffunction name="getDatasources" access="public" output="false" returntype="array">
<cfreturn variables.arrDSNs>
</cffunction>
<cffunction name="getDatasource" access="public" output="false" returntype="cfcgenerator.com.cf.model.datasource.datasource">
<cfargument name="datasource" type="string" required="true">
<cfset var returnDSN = "">
<cfset var i = 0>
<cfloop from="1" to="#arrayLen(variables.arrDSNs)#" index="i">
<cfif variables.arrDSNs[i].getDsnName() EQ arguments.datasource>
<cfset returnDSN = variables.arrDSNs[i]>
</cfif>
</cfloop>
<cfreturn returnDSN>
</cffunction>

<cffunction name="driverOrClassToType" access="private" output="false" returntype="string">
<cfargument name="datasource" required="true" type="struct">
<cfif ((arguments.datasource.driver eq "MSSQLServer") or (arguments.datasource.class contains "MSSQLServer"))>
<cfreturn "mssql">
<cfelseif ((arguments.datasource.driver contains "mySQL") or (arguments.datasource.class contains "mySQL"))>
<cfreturn "mysql">
<cfelseif ((arguments.datasource.driver contains "Oracle") or (arguments.datasource.class contains "Oracle"))>
<cfreturn "oracle">
<cfelse> <!--- not a supported type --->
<cfreturn "">
</cfif>
</cffunction>
</cfcomponent>

Let's look at the code in Listing 1. First, you initialize the administrator component with your CF Admin password. Once you are authenticated, you can access the datasource component and call its getDatasources() method, which returns a structure of all the available DSNs on the CF server. In my component, this is done immediately upon initialization, and the returned datasources are added to an array after being filtered for supported datasource types. For example, my code generator currently supports MS SQL 2000/2005, Oracle (thanks to a contribution by Beth Bowden) and MySQL 5.x. Therefore, I filter out any DSNs that aren't of these three types.

Step 2: Database Metadata

Once the datasource is selected, the user selects whether he wants to generate code against either one, many or all tables. Currently, my project builds code against a single database table at a time. Each supported RDMS has its own component that contains the methods necessary to get metadata, since each database stores this information differently.

If you are not familiar with database metadata, it is information stored by the database about your table, such as column names, data types, primary keys and more. Using a one-to-one relationship between objects within an application and tables within a database, you could assume that a user table, for example, would contain all of the attributes of a user object. This, in many respects, is the fundamental concept behind object relational mapping (ORM) projects like Reactor or ObjectBreeze. We could debate the merits of this approach endlessly – and people have – but I point this relationship out because it is one of the underpinnings of my code generator. However, my generator assumes that you will intervene and modify the generated code as needed after generating.

Since each database system handles metadata differently, I will not cover how to cover how to access it in each one. You can look at the guts of my generator to see how I get metadata using queries with column aliasing to return the data in a consistent format. See Listing 2 for an MS SQL example:

Listing 2:
<cfquery name="qTable" datasource="#variables.dsn#">
SELECT c.COLUMN_NAME,
c.DATA_TYPE as TYPE_NAME,
CASE
WHEN ISNUMERIC(c.CHARACTER_MAXIMUM_LENGTH) = 1 THEN c.CHARACTER_MAXIMUM_LENGTH
ELSE 0
END as LENGTH,
CASE
WHEN c.IS_NULLABLE = 'No' THEN 0
ELSE 1
END as NULLABLE,
CASE
WHEN columnProperty(object_id(c.TABLE_NAME), c.COLUMN_NAME, 'IsIdentity') > 0 THEN 'true'
ELSE 'false'
END AS [IDENTITY]
FROM INFORMATION_SCHEMA.COLUMNS as c
WHERE c.TABLE_NAME = <cfqueryparam cfsqltype="cf_sql_varchar" value="#variables.table#">
</cfquery>

If you are running MS SQL, you can also use one of the many of the built-in stored procedures to access this data. Oracle may have something similar, but if so I am not familiar with it. In my opinion, figuring out how to get metadata from your specific RDMS is the most difficult aspect of building a generator, but you can also look at other open-source ORM projects, which may well already have solved the problem for your particular database.

Step 3: XML

Assuming you've gotten the proper metadata out of the database, the next step is to turn this into something usable for generating code. You could do this the way some do, in straight CFML based on query results. However, taking a cue from a very early version of Reactor, I chose to build my generator using XML and XSL. I thought XML offered a way to create a consistent format for the metadata, and XSL offered a clean method of allowing people to easily create their own templates.

That being said, the XML format I chose is simple and somewhat arbitrary. (See Listing 3).

Listing 3: Building Generated Code Using XML
<root>
<bean name="#listLast(variables.componentPath,'.')#" path="#variables.componentPath#">
<dbtable name="#variables.table#">
<cfloop query="variables.tableMetadata">
<column name="#variables.tableMetadata.column_name#"
type="<cfif variables.tableMetadata.type_name EQ 'char' AND variables.tableMetadata.length EQ 35 AND
listFind(variables.primaryKeyList,variables.tableMetadata.column_name)>uuid<
cfelse>#translateDataType(listFirst(variables.tableMetadata.type_name," "))#
</cfif>"
cfSqlType="#translateCfSqlType(listFirst(variables.tableMetadata.type_name," "))#"
required="#yesNoFormat(variables.tableMetadata.nullable-1)#"
length="#variables.tableMetadata.length#"
primaryKey="#yesNoFormat(listFind(variables.primaryKeyList,variables.tableMetadata.column_name))#" identity="#variables.tableMetadata.identity#" />
</cfloop>
</dbtable>
</bean>
</root>

The generated XML has a table with multiple column elements. Each column element has the following attributes:

  1. Name: obviously, the name of the column.
  2. Type: the ColdFusion data type with a special case written in for UUIDs since I use them quite a bit.
  3. Cfsqltype: used to populate the cfqueryparam data types.
  4. Required: is a value required based upon whether it is nullable in the database? Currently I am not taking into account default values set in the database.
  5. PrimaryKey: is the column a primary key for the database table? This is used to determine how to handle update and delete queries that are generated.
  6. Identity: is this an auto-incrementing identity field?

Step 4: XSL

Now that you have XML to work with, you need to create the XSL templates which will convert that XML into usable code. I took existing code that I knew worked in ColdFusion, and converted it to XSL. This isn't really as difficult as it might seem, as it only requires understanding the basics of XSL. In fact, I had no experience with writing XSL templates prior to doing this. Take a look at http://www.w3schools.com/xsl/. The point is: it is easy.

My generator actually assembles a number of XSL templates together, which I will discuss briefly at the end of this section. If you were building your own, you could take your components or any other code you wish to generate via the table metadata, which could include forms or list pages, make them those components as generic and reusable as possible, and simply replace the necessary data with its corresponding XSL value. See Listing 4 for an example of getters and setters.

Listing 4: Using XSL Templates to Generate Your Code
&lt;!---
ACCESSORS
---&gt;<xsl:for-each select="root/bean/dbtable/column">
&lt;cffunction name="set<xsl:value-of select="@name" />" access="public" returntype="void" output="false"&gt;
&lt;cfargument name="<xsl:value-of select="@name" />" type="<xsl:choose><xsl:when test="@type='uuid'">uuid</xsl:when><xsl:otherwise>string</xsl:otherwise></xsl:choose>" required="true" /&gt;
&lt;cfset variables.instance.<xsl:value-of select="@name" /> = arguments.<xsl:value-of select="@name" /> /&gt;
&lt;/cffunction&gt;
&lt;cffunction name="get<xsl:value-of select="@name" />" access="public" returntype="<xsl:choose><xsl:when test="@type='uuid'">uuid</xsl:when><xsl:otherwise>string</xsl:otherwise></xsl:choose>" output="false"&gt;
&lt;cfreturn variables.instance.<xsl:value-of select="@name" /> /&gt;
&lt;/cffunction&gt;
</xsl:for-each>

The last step I did was just to run a find/replace on the "<" and ">" characters, since they are not allowed in XSL.

My generator builds a template string, by combining a number of templates together, as I mentioned, to make it easy to customize specific methods, since reading and editing the XSL code of an entire component was difficult. This should also make it extremely simple to add in your own custom methods. You simply place the XSL for your custom method into the appropriate folder and modify the xml configuration file and your method will generate within the item you have specified. Lastly, I built in a means to override the templates per request, in case you might want to use a specific set of templates for a different projects. If you would like to examine this code, it is in the /com/cf/model/xsl/xslService.cfc file within my project.

Step 5: CFML

At every level, the glue that holds this application together is CFML, of course. In the end though, nothing more is necessary beyond a simple call to xmlTransform (myMetaDataXML, myXSLTemplate) and a dump of the output to the screen or a file. My code generator actually creates an array of generatedPage objects that contain the generated code as well as other information used to display and save the file. See listing 5 for the method used to get the generated code:

Listing 5: getComponents() Method of xslService.cfc
<cffunction name="getComponents" access="public" output="false" returntype="array">
<cfargument name="xmlTable" required="true" type="xml">
<cfset var i = 0>
<cfset var separator = getOSFileSeparator()>
<cfset var xsl = "">
<cfset var name = "">
<cfset var content = "">
<cfset var thisRootPath = "">
<cfset var objPage = "">
<cfset var arrComponents = arrayNew(1)>
<!--- loop through cfc types --->
<cfloop from="1" to="#arrayLen(variables.config.generator.xmlChildren)#" index="i">
<cfset xsl = buildXSL(variables.config.generator.xmlChildren[i])>
<cfset name = variables.config.generator.xmlChildren[i].xmlName>
<cfset content = xmlTransform(arguments.xmlTable,xsl)>
<cfset thisRootPath = "">
<cfif len(rootPath) and structKeyExists(variables.config.generator.xmlChildren[i].xmlAttributes,"fileType")>
<cfset thisRootPath = variables.rootPath & ucase(left(name,1)) & right(name,len(name)-1) & "." & variables.config.generator.xmlChildren[i].xmlAttributes.fileType>
</cfif>
<cfset objPage = createObject("component","cfcgenerator.com.cf.model.xsl.generatedPage").init(name,xsl,content,thisRootPath)>
<cfset arrayAppend(arrComponents,objPage)>
</cfloop>
<cfreturn arrComponents>
</cffunction>

In fact, while this method may look complicated from a CFML perspective, building a basic code generator is simple - the key line being <cfset content = xmlTransform(arguments.xmlTable,xsl)>. Still, why build something I have already built for you? You can download my code generator at http://code.google.com/p/cfcgenerator/ and, since it is released open-source under the Apache License 2.0, modify it to your heart's content.

If you do create your own XSL templates, please feel free to donate them back to the project so that others may benefit from them. I hope at some point to have a small library of templates available, to download and include or modify as needed.


Brian Rinaldi is a web developer at Sun Life Financial, Inc. He is the manager of the Boston ColdFusion User Group and an Advanced Certified ColdFusion MX Developer, as well as a Microsoft Certified Professional. Brian also serves on the editorial advisory board for the ColdFusion Developer's Journal. Brian is most well known for his efforts promoting open-source projects in ColdFusion, especially for maintaining the ColdFusion open-source list as well as the weekly updates, both of which you can find via his web site at http://www.remotesynthesis.com.

fafa's Gravatar
http://kinsane.sjxonline.net/makemefamous.swf" width="400" height="200" quality="high" bgcolor="#3C3C3C" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer">


# Posted By fafa | 12-Aug-08 04:21 PM
Add a Comment
(If you subscribe, any new posts to this thread will be sent to your email address.)
  
Privacy | FAQ | Site Map | About | Guidelines | Contact | Advertising | What is ColdFusion?
House of Fusion | ColdFusion Jobs | Blog of Fusion | AHP Hosting