ColdFusion Metadata Magic: Part One

December 11, 2007

by Brice Mason

Home » Articles

Developers are writers. Each day we are tasked with telling a story through the software we create to a varied audience including other developers, technical writers, managers, and users. In addition to the code we create is usually a set of documentation which is produced with the same amount of care and attention. While I'm sure most of us would rather create content which includes semi-colons and curly braces, well-written, consistent documentation is one of the keys to a fluid software development team. The task of documentation doesn't have to be as daunting as once thought. This article will show you how to generate, maintain, and version standard documentation for all your ColdFusion components (CFCs) automatically in less than 100 lines of code.

So What's the Big Deal?

There's no doubt that creating good software is hard work. To produce documentation matching the caliber of the software it intends to describe is twice as hard. Whether you're in a team of 1 or 100, have a technical writer on staff or not, your documentation can at times make or break you. How many times have you revisited your own code six months down the road and thought, "What the heck is this doing?" Further, while working in a team environment we are constantly exchanging code and documentation between members. Maintaining consistent documentation will keep the questions and kickbacks to a minimum and forward-thinking innovation to the maximum.

How We Usually Do It

As most developers are already aware, ColdFusion MX ships with functionality which enables us to maintain basic metadata about our CFCs which can then be used in utilities like the ColdFusion Component Explorer for reporting. Standard practice includes using the hint attribute in tags such as cfcomponent, cffunction, cfargument, and cfproperty to describe the functionality of the target object.

To demonstrate, we'll use the Profile component as shown in Listing 1 below. This component is used to maintain user preference information such as user id, name, and favorite color. It consists of three self-explanatory functions, init, setProfile, and getProfile. It's a simple component and we've done our due diligence by using the hint attribute where appropriate. When we invoke this component directly in our web browser, we get a result as shown in Figure 1. The documentation produced by ColdFusion is good, certainly consistent, but lacks flexibility in its reporting.

<cfcomponent displayname="Profile" output="no" hint="Manages profile information for a given user such as name and favorite color.">

    <!--- initialize the profile structure --->
    <cfset variables._st_profile = StructNew() />
    <cfset StructInsert( variables._st_profile, "userId",        0      ) />
    <cfset StructInsert( variables._st_profile, "name",          ""     ) />
    <cfset StructInsert( variables._st_profile, "favoriteColor", "none" ) />
    
    <!--- ====================================================
		Function: init
    ===================================================== --->
    <cffunction name="init" access="public" output="no" returntype="Profile" hint="Creates a new profile for a given user.">
    	<cfargument name="userId" type="numeric" required="yes" />
        
        <!--- retrieve the profile information --->
        <cfquery name="q_getProfile" datasource="theDsn">
            select userId,
                   name,
                   favoriteColor
            from profile
            where userId = #arguments.userId#
        </cfquery>
        
        <cfif q_getProfile.RecordCount eq 1>
        	<cfset variables.setProfile( q_getProfile ) />
        </cfif>
        
        <cfreturn this />
    </cffunction>
    <!--- ---------- init ------------------------------- --->
    
    <!--- ====================================================
		Function: setProfile
    ===================================================== --->
    <cffunction name="setProfile" access="private" output="no" returntype="void" hint="Sets the profile information.">
    	<cfargument name="q_profile" type="any" required="yes" />
        
        <cfset StructUpdate( variables._st_profile, "userId",        arguments.q_profile.userId        ) />
        <cfset StructUpdate( variables._st_profile, "name",          arguments.q_profile.name          ) />
        <cfset StructUpdate( variables._st_profile, "favoriteColor", arguments.q_profile.favoriteColor ) />
    
    	<cfreturn />
    </cffunction>
    <!--- ---------- setProfile ------------------------- --->
    
    <!--- ====================================================
		Function: getProfile
    ===================================================== --->
    <cffunction name="getProfile" access="public" output="no" returntype="struct" hint="Returns a structure of profile information.">
    	<cfreturn variables._st_profile />
    </cffunction>
    <!--- ---------- getProfile ------------------------- --->

</cfcomponent>

Listing 1 - Profile.cfc

Figure 1 - Component Explorer Output of Profile.cfc

As listed in the ColdFusion reference documentation, we have two key features which offer incredible flexibility to us. We have the ability to add arbitrary bits of metadata to the tags that make up our CFCs in the form of user-defined attributes, and we have the cfproperty tag which may be used to store user-defined metadata for components not to be consumed as web services. This opens the door for consistent maintenance of information normally found in embedded comments such as component author information, status messages, and revision history. However, if you use the Component Explorer for the reporting of this metadata, you'll soon discovery that the reporting of this extra data you spent time to create is lost.

A Better Way

Having explored the commonly known and practiced features of ColdFusion as well as identified constraints relating to the management and reporting of CFC metadata, let's work to make it better. To demonstrate, we'll use the Documenter component as shown in Listing 2. This component can be hooked in to all your existing components with one line of code and it will instantly build a repository of standard documentation, checking for updates to the metadata which describes your CFCs, and build new versions as necessary. All you have to worry about is filling in as much metadata as you want to describe your code, the Documenter handles the rest. To hook the Documenter in to existing components, simply insert the code from Listing 3 into the constructor section of the CFC you want it to manage. As a matter of style, it is best to insert this code as the last line of your constructor section, just before the closing cfcomponent tag. The path to the Documenter component is dependent on your environment. Let's take a closer look at how the Documenter works.

<cfcomponent displayname="Documenter" output="yes" hint="Manages documentation for ColdFusion components.">

    <!--- constants --->
    <!--- define the standard name and path of the root of the repository --->
    <cfset variables._REPOSITORY_DIRECTORY = "documenter-repository"  />
    <cfset variables._REPOSITORY_PATH = GetDirectoryFromPath( GetMetaData( this ).path ) & variables._REPOSITORY_DIRECTORY />
    
    <cfdump var="#variables._REPOSITORY_PATH#" />
    
    <!--- create the repository root directory if necessary --->
    <cfif not DirectoryExists( variables.getRepositoryPath() )>
        <cfdirectory action="create" directory="#variables.getRepositoryPath()#" />
    </cfif>

	<!--- ====================================================
		Function: generateDocs
	===================================================== --->
	<cffunction name="generateDocs" access="public" output="no" returntype="void">
    	<cfargument name="componentInstance" type="any" required="yes" />
        
        <cfset var local = StructNew() />
        <cfset local.metaData = GetMetaData( arguments.componentInstance ) />
        
        <!--- serialize the component metadata to wddx --->
        <cfwddx action="cfml2wddx" input="#local.metaData#" output="local.wddx_metaData" />
                
        <!--- get the 'signature' of the metaData --->
        <cfset local.signature = hash( local.wddx_metaData, "md5" ) />
        
        <!--- configure the path to the component instance's root under the repository --->
        <cfset local.componentPath = ListAppend( variables.getRepositoryPath(), local.metaData.name, "\" ) />
        
        <!--- create the component instance's directory if necessary --->
        <cfif not DirectoryExists( local.componentPath )>
            <cfdirectory action="create" directory="#local.componentPath#" />
        </cfif>
        
        <!--- define the absolute path to the component instance's file. --->
        <cfset local.componentFile = ListAppend( local.componentPath, local.signature & ".xml", "\" ) />
        
        <!--- if this is a new version, write to file --->
        <cfif not FileExists( local.componentFile )>
        	<cffile action="write"
            		file="#local.componentFile#"
                    output="#local.wddx_metaData#" />
        </cfif>        
        
    	<cfreturn />
    </cffunction>
    <!--- ---------- generateDocs ----------------------- --->
    
    <!--- ====================================================
		Function: getRepositoryPath
	===================================================== --->
    <cffunction name="getRepositoryPath" access="public" output="no" returntype="string">
    	<cfreturn variables._REPOSITORY_PATH />
    </cffunction>
    <!--- ---------- getRepositoryPath ------------------ --->

</cfcomponent>

Listing 2 - Documenter.cfc

<cfset CreateObject( "component", "Documenter" ).generateDocs( this ) />

Listing 3 - Installing the Documenter into Existing Components

The Documenter component is quite simple and begins with the creation of the root repository directory named documenter-repository by default. The repository is stored in the same directory as the Documenter component resides. Beneath the root of the repository will be a group of directories each named after the component for which the Documenter manages its metadata. As demonstrated earlier, the function which is used to manage the connection between existing components and the Documenter is the generateDocs function which is what we'll describe next.

The Brains — generateDocs

The generateDocs function is passed a component instance, or object. Using the ColdFusion function GetMetaData, we have the ability to obtain all the metadata from the object. This includes information such as where the component is stored, what other components it may be derived from, function definitions, and any user-defined metadata. The generateDocs function takes this data structure and serializes it to WDDX (Web Distributed Data eXchange) format using the cfwddx tag. WDDX, a technology developed by Allaire, is an XML representation of data structures. Now that we have this information in a structured format, we need to assign it some sort of unique attribute, a signature which can be used to determine when the data has changed. An easy and rather elegant way to do this is with the ColdFusion hash function, which is simply a method used to convert an arbitrary string of data to a unique fixed-length string which acts as a sort of fingerprint or signature. While we have a couple of algorithms available to use with this function, we'll use md5. You're probably familiar with this type of algorithm as it is commonly used to produce digital signatures of assets you download every day to validate that the source is as expected. Having gained the unique signature of the component metadata, the Documenter component will check the repository for the given component to see if a file exists with the same signature. If not, then the WDDX data is simply written to file with the hash signature as its filename, producing the newest version.

Extending Component Metadata

As mentioned earlier, ColdFusion offers us a means to define additional metadata to describe our components in the form of attributes to component tags or cfproperty tags. We are accustomed to loading our code with embedded comments which describe who wrote the code, when they did it, who they wrote it for, and so on. As this information changes, we are left with just the most recent information. We can't see how this information has evolved over time which restricts its intended use.

Using cfproperty

As described in the ColdFusion reference material, the cfproperty tag when defined in components not to be consumed as web services, may be used to extend the component metadata. This means that this information is included in a call to the GetMetaData function used in the Documenter and will serve as an excellent choice to migrate the data we previously managed using embedded comments to the Documenter repository.

Listing 4 (see below) provides three sets of properties commonly found in embedded comments. Generally as you create your own properties, you will want to be aware of two things regarding the cfproperty tag. One, ensure that the name attribute is unique, and two, if you want to describe the type of property, don't use the inherently supported type attribute, as arbitrary values are assumed to be component data types. The examples in Listing 4 use the user-defined attribute propertyType.

<!--- author information --->
<cfproperty name="authorName"   propertytype="authorInfo" value="Brice Mason"              />
<cfproperty name="authorEmail"  propertytype="authorInfo" value="bmason@stegoworks.com"     />
<cfproperty name="authorWww"    propertytype="authorInfo" value="http://developer.stegoworks.com" />
<cfproperty name="companyName"  propertytype="authorInfo" value="Stegoworks LLC"           />
<cfproperty name="supportEmail" propertytype="authorInfo" value="support@stegoworks.com"   />

<!--- status messages --->
<cfproperty name="status-101" messagetype="warning" messagegroup="fileSystem" messagecode="101"
               message="Disk space is approaching its maximum." />
<cfproperty name="status-102" messagetype="error"   messagegroup="fileSystem" messagecode="102" message="The file does not exist." />

<!--- revision history --->
<cfproperty name="revision-01" propertytype="revisionHistory" author="Brice Mason" date="2007-08-23" entry="Creation date." />
<cfproperty name="revision-02" propertytype="revisionHistory" author="Chase Paul"  date="2007-08-24" entry="Fixed a bug."   />

Listing 4 - Recommended User-Defined Metadata

The first set of properties defined in Listing 4 describes who wrote the component and for whom. This information would include obviously the name of the author, who the author works for, how to contact the author, and so on. This would serve as a particularly useful piece of information for companies that outsource development to contractors or just have large development teams. Since the information is versioned in a centralized repository, it is easy and useful to look back and find who authored what and when.

The second set of properties defined in Listing 4 describe the various messages that would come from interaction with the component. This is an invaluable tool, as it eliminates searching through code that may be thousands of lines long for every cfthrow or return code and message. The information is not only centrally defined at the top of the component, but it will also be stored and managed in the Documenter repository.

The third and final set of properties defined in Listing 4 provides for a running log of revision history entries. This is perhaps one of the most important and powerful set of properties you can define. The code we create is a living, breathing object. By defining the descriptions of how these objects change and have them stored and versioned on the spot without our intervention allows us to tell the right story with little effort.

Next Steps

We've explored the inherently rich capability of ColdFusion to define and report on component metadata and successfully exploited the flexibility these features expose. Although we now have a repository full of rich XML data which represents our components, we still haven't come up with a way to report on it. Check back for part two to this article when we explore the powerful features of XPath and XSL Transformations to create our CFC documentation.

For more information on materials related to this article, dive back into the CFML reference documentation.

Resources