ColdFusion Iterating Business Objects (IBOs) From The Ground-Up

<cfcomponent
	displayname="AbstractIBO"
	output="false"
	hint="This handles the base functionality of an Iterating Business Object.">
 
 
	<!--- Run the pseudo constructor to set up default data structures. --->
	<cfscript>
 
		// Set up the instance object in the private memory scope.
		VARIABLES.Instance = StructNew();
 
		// This is the index of the current iteration.
		VARIABLES.Instance.IterationIndex = 1;
 
		// These are the list of keys that can be "get"ed and "set"ed in some
		// way. We are not going to define how they are accessed at this moment.
		// That will be done once the underlying data structure is set. These will
		// populate the Get/Set structures (that follow).
		VARIABLES.Instance.GetAttributesList = "*";
		VARIABLES.Instance.SetAttributesList = "*";
 
		// This is a structure of keys that can be "get"ed and "set"ed. These
		// will be populated based on the attributes list above.
		VARIABLES.Instance.GetAttributes = StructNew();
		VARIABLES.Instance.SetAttributes = StructNew();
 
		// This is a structure that will alias the getter and setter methods.
		// We are doing this so that we don't have to care about any method
		// mapping.
		VARIABLES.Instance.GetAttributeMethods = StructNew();
		VARIABLES.Instance.SetAttributeMethods = StructNew();
 
 
		// This is the underlying data object. I am initializing to an empty qyery.
		// I can't believe this actually works, but at least it puts a query object
		// as this variable value.
		VARIABLES.Instance.RecordSet = QueryNew( "" );
 
 
		// Set up the non-instance private data.
 
		// These are the types of getter/setters that can be accessed/mutated.
		// Right now, a value can either be accessed as a property or as a
		// function of the underlying data structure. These values are meant to
		// be constant and are therefore not part of the instance variable struct.
		VARIABLES.KeyTypes = StructNew();
 
		// This defines a key as being accessed as a method.
		VARIABLES.KeyTypes.METHOD = 1;
 
		// This defines a key as being accessed as a property.
		VARIABLES.KeyTypes.PROPERTY = 2;
 
 
		// Set up the default public data.
 
		THIS.RecordCount = 0;
		THIS.CurrentRow = 0;
 
	</cfscript>
 
 
	<cffunction name="Init" access="public" returntype="AbstractIBO" output="false"
		hint="Returns an initialized Abstract IBO instance.">
 
		<!--- Return This reference. --->
		<cfreturn THIS />
	</cffunction>
 
 
	<cffunction name="First" access="public" returntype="void" output="false"
		hint="Resets the IBO cursor to point to the first record in the record set.">
 
		<!--- Set the index. --->
		<cfset VARIABLES.Instance.IterationIndex = 1 />
 
		<!--- Set the public data. --->
		<cfset THIS.CurrentRow = 1 />
 
		<!--- Return out. --->
		<cfreturn />
	</cffunction>
 
 
	<cffunction name="Get" access="public" returntype="any" output="false"
		hint="Gets the value at the given key. Throws an error if the key is not available.">
 
		<!--- Define arguments. --->
		<cfargument name="Key" type="string" required="true" />
 
		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />
 
		<!--- Check to see if the key is available. --->
		<cfif StructKeyExists( VARIABLES.Instance.GetAttributes, ARGUMENTS.Key )>
 
			<!--- This key is valid. Check to see how it is being accessed. --->
			<cfif (VARIABLES.Instance.GetAttributes[ ARGUMENTS.Key ] EQ VARIABLES.KeyTypes.PROPERTY)>
 
				<!--- Return the property straight out of the record set. --->
				<cfreturn VARIABLES.Instance.RecordSet[ ARGUMENTS.Key ][ VARIABLES.Instance.IterationIndex ] />
 
			<cfelse>
 
				<!---
					The key is being accessed as a method. Get a pointer to the method.
					We need to get the interim method since ColdFusion cannot handle the
					parsing of an array notation followed by a method call.
				--->
				<cfset LOCAL.Method = VARIABLES.Instance.GetAttributeMethods[ ARGUMENTS.Key ] />
 
				<!--- Return the value of the get method. --->
				<cfreturn LOCAL.Method() />
 
			</cfif>
 
		<cfelse>
 
			<!--- The key is not available for getting. Throw an error. --->
			<cfthrow
				message="You have attempted to access an invalid key."
				type="IBO.InvalidGetKey"
				detail="The key you are trying to get, which is currently #UCase( ARGUMENTS.Key )#, is not available in this IBO."
				/>
 
		</cfif>
	</cffunction>
 
 
	<cffunction name="GetAccessHints" access="public" returntype="struct" output="false"
		hint="This is meant for debugging so you can see how the properties are being accessed.">
 
		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />
 
		<!--- Create a structure to return both the getter and setter info. --->
		<cfset LOCAL.Hints = StructNew() />
 
		<!--- Set the keys so that the access methods make sense. --->
		<cfset LOCAL.Hints.Keys = Duplicate( VARIABLES.KeyTypes ) />
 
		<!--- Set the getters and setters. --->
		<cfset LOCAL.Hints.Get = Duplicate( VARIABLES.Instance.GetAttributes ) />
		<cfset LOCAL.Hints.Set = Duplicate( VARIABLES.Instance.SetAttributes ) />
 
		<!--- Return the hints. --->
		<cfreturn LOCAL.Hints />
	</cffunction>
 
 
	<cffunction name="GetPropertyList" access="public" returntype="string" output="false"
		hint="Returns the property list.">
 
		<cfreturn VARIABLES.Instance.GetAttributesList />
	</cffunction>
 
 
	<cffunction name="IsFirst" access="public" returntype="boolean" output="false"
		hint="Determines if the IBO is pointing at the first record.">
 
		<cfreturn (VARIABLES.Instance.IterationIndex EQ 1) />
	</cffunction>
 
 
	<cffunction name="IsLast" access="public" returntype="boolean" output="false"
		hint="Determines if the IBO is pointing at the last record.">
 
		<cfreturn (VARIABLES.Instance.IterationIndex EQ VARIABLES.Instance.RecordSet.RecordCount) />
	</cffunction>
 
 
	<cffunction name="IsRecord" access="public" returntype="boolean" output="false"
		hint="Determines if the IBO is pointing at a valid record. If the IBO goes past the last index of the current record set, this will return false.">
 
		<cfreturn (VARIABLES.Instance.IterationIndex LTE VARIABLES.Instance.RecordSet.RecordCount) />
	</cffunction>
 
 
	<cffunction name="LoadQuery" access="public" returntype="any" output="false"
		hint="This loads the given query data into the IBO.">
 
		<!--- Define arguments. --->
		<cfargument name="RecordSet" type="query" required="true" />
 
		<cfscript>
 
			// Define the local scope.
			var LOCAL = StructNew();
 
			// Duplicate the query when storing it in the IBO. I am not sure that
			// I completely agree with this, but it seems that this is (in part)
			// what the original code is doing. We are doing this so that even if
			// the query is altered after it is sent it, it does not affect the
			// underlying data of the IBO.
			VARIABLES.Instance.RecordSet = Duplicate( ARGUMENTS.RecordSet );
 
			// Now that we have the data stored in our IBO, we can populate the
			// structures of getters and setters. If the list is "*" then we
			// are going to copy the record set's column list over into the list
			// of getters and setters. Otherwise, we will keep it as is.
 
			// Set local flags for select all.
			LOCAL.IsGetAll = VARIABLES.Instance.GetAttributesList.Matches( "\*" );
			LOCAL.IsSetAll = VARIABLES.Instance.SetAttributesList.Matches( "\*" );
 
			// Check to see if we are getting all properties.
			if (LOCAL.IsGetAll){
 
				// The Get attributes list will be the record set's column list.
				VARIABLES.Instance.GetAttributesList = VARIABLES.Instance.RecordSet.ColumnList;
 
			}
 
			// Check to see if we are setting all properties.
			if (LOCAL.IsSetAll){
 
				// The Set attributes list will be the record set's column list.
				VARIABLES.Instance.SetAttributesList = VARIABLES.Instance.RecordSet.ColumnList;
 
			}
 
 
			// At this point, we have the get and set attributes list which have
			// been populated either by the record set OR hard coded by the concrete
			// extending IBO. Let's store these keys into the get and set attributes
			// structures for faster lookup and editing.
 
			// Loop over get property list.
			for (
				LOCAL.PropertyIndex = 1 ;
				LOCAL.PropertyIndex LTE ListLen( VARIABLES.Instance.GetAttributesList ) ;
				LOCAL.PropertyIndex = (LOCAL.PropertyIndex + 1)
				){
 
				// Get the current property.
				LOCAL.Property = ListGetAt( VARIABLES.Instance.GetAttributesList, LOCAL.PropertyIndex );
 
				// Set the property to a default value.
				VARIABLES.Instance.GetAttributes[ LOCAL.Property ] = 0;
			}
 
			// Loop over set property list.
			for (
				LOCAL.PropertyIndex = 1 ;
				LOCAL.PropertyIndex LTE ListLen( VARIABLES.Instance.SetAttributesList ) ;
				LOCAL.PropertyIndex = (LOCAL.PropertyIndex + 1)
				){
 
				// Get the current property.
				LOCAL.Property = ListGetAt( VARIABLES.Instance.SetAttributesList, LOCAL.PropertyIndex );
 
				// Set the property to a default value.
				VARIABLES.Instance.SetAttributes[ LOCAL.Property ] = 0;
			}
 
 
			// Now, let's collect the IBO methods that have been mapped to properties.
			// This is done to allow for differences in naming conventions between the
			// database (which results in the record set) and the CFC methods. For
			// instance, the database might have "first_name", but for naming convention's
			// sake, we can't have a method named "GetFist_Name". To accomodate this,
			// we are forcing the methods to use the "mapto" attribute. Method's without
			// this attribute (for both GET and SET) will be ignored.
 
			// Loop over the methods to check for anything that begins with "get" or "set".
			for (LOCAL.CFCProperty in VARIABLES){
 
				// Check to see if this is a user-defined function. We only care
				// about things that can be called as functions.
				if (
					IsCustomFunction( VARIABLES[ LOCAL.CFCProperty ] ) AND
					StructKeyExists( GetMetaData( VARIABLES[ LOCAL.CFCProperty ] ), "MapTo" ) AND
					(
						(Left( LOCAL.CFCProperty, 3 ) EQ "GET") OR
						(Left( LOCAL.CFCProperty, 3 ) EQ "SET")
					)){
 
					// Get the mapto attribute value.
					LOCAL.MapTo = GetMetaData( VARIABLES[ LOCAL.CFCProperty ] ).MapTo;
 
					// Check to see if this is a get method and that it is in the defined
					// list of "get"able properties.
					if (
						(Left( LOCAL.CFCProperty, 3 ) EQ "GET") AND
						(
							LOCAL.IsGetAll OR
							StructKeyExists( VARIABLES.Instance.GetAttributes, GetMetaData( VARIABLES[ LOCAL.CFCProperty ] ).MapTo )
						)){
 
						// Flag this property as being accessed as a method.
						VARIABLES.Instance.GetAttributes[ LOCAL.MapTo ] = VARIABLES.KeyTypes.METHOD;
 
						// Set the method alias.
						VARIABLES.Instance.GetAttributeMethods[ LOCAL.MapTo ] = VARIABLES[ LOCAL.CFCProperty ];
 
					// Check to see if this is a set method and that it is in the defined
					// list of "set"able properties.
					} else if (
						(Left( LOCAL.CFCProperty, 3 ) EQ "SET") AND
						(
							LOCAL.IsSetAll OR
							StructKeyExists( VARIABLES.Instance.SetAttributes, GetMetaData( VARIABLES[ LOCAL.CFCProperty ] ).MapTo )
						)){
 
						// Flag this property as being accessed as a method.
						VARIABLES.Instance.SetAttributes[ LOCAL.MapTo ] = VARIABLES.KeyTypes.METHOD;
 
						// Set the method alias.
						VARIABLES.Instance.SetAttributeMethods[ LOCAL.MapTo ] = VARIABLES[ LOCAL.CFCProperty ];
 
					}
 
				}
 
			}
 
 
			// Convert the record set column list to a structure for faster access.
			LOCAL.Columns = StructNew();
 
			// Loop over columns and add keys to column struct.
			for (
				LOCAL.ColumnIndex = 1 ;
				LOCAL.ColumnIndex LTE ListLen( VARIABLES.Instance.RecordSet.ColumnList ) ;
				LOCAL.ColumnIndex = (LOCAL.ColumnIndex + 1)
				){
 
				// Add to struct. The value here is not important.
				LOCAL.Columns[ ListGetAt( VARIABLES.Instance.RecordSet.ColumnList, LOCAL.ColumnIndex ) ] = 0;
			}
 
 
			// At this point, we should already have all the mapped methods accounted for.
			// Now, we have to figure out which of the properties will be accessed as a
			// property (and which ones are not valid in any access form). To get this,
			// let's loop over the get and set properties and for each property that is
			// not being accessed as a method, check to see if there is an equivalent
			// record set column.
 
			// Loop over get attributes.
			for (LOCAL.Property in VARIABLES.Instance.GetAttributes){
 
				// Check to see if this attribute is zero. This would indicate a
				// property that has not been validated yet (and is just based on
				// the list of properties).
				if (NOT VARIABLES.Instance.GetAttributes[ LOCAL.Property ]){
 
					// This property has not been accounted for. Check for a
					// matching record set column.
					if (StructKeyExists( LOCAL.Columns, LOCAL.Property )){
 
						// There is a matching record set column. Set this property as
						// being accessed as a standard property.
						VARIABLES.Instance.GetAttributes[ LOCAL.Property ] = VARIABLES.KeyTypes.PROPERTY;
 
					} else {
 
						// This property has not been accounted for by either the mapped
						// methods or the record set columns. Delete this property from
						// the get attributes.
						StructDelete( VARIABLES.Instance.GetAttributes, LOCAL.Property );
 
					}
 
				}
 
			}
 
 
			// Loop over set attributes.
			for (LOCAL.Property in VARIABLES.Instance.SetAttributes){
 
				// Check to see if this attribute is zero. This would indicate a
				// property that has not been validated yet (and is just based on
				// the list of properties).
				if (NOT VARIABLES.Instance.SetAttributes[ LOCAL.Property ]){
 
					// This property has not been accounted for. Check for a
					// matching record set column.
					if (StructKeyExists( LOCAL.Columns, LOCAL.Property )){
 
						// There is a matching record set column. Set this property as
						// being accessed as a standard property.
						VARIABLES.Instance.SetAttributes[ LOCAL.Property ] = VARIABLES.KeyTypes.PROPERTY;
 
					} else {
 
						// This property has not been accounted for by either the mapped
						// methods or the record set columns. Delete this property from
						// the Set attributes.
						StructDelete( VARIABLES.Instance.SetAttributes, LOCAL.Property );
 
					}
 
				}
 
			}
 
 
			// At this point, we have set up the Get and Set attributes based on the
			// mapped methods and the record set column list. There might be some
			// differences between the hard coded set/get lists and the available
			// Set and Get attributes. Not sure how to handle that at the moment. I
			// am going to leave it as-is for now. However, we could certainly set
			// the get and let list based on the keys of the attribute structrures:
			//
			// For example:
			// VARIABLES.Instance.GetAttributesList = StructKeyList( VARIABLES.Instance.GetAttributes );
			// VARIABLES.Instance.SetAttributesList = StructKeyList( VARIABLES.Instance.SetAttributes );
 
			// Set public data.
			THIS.RecordCount = VARIABLES.Instance.RecordSet.RecordCount;
			THIS.CurrentRow = 1;
 
 
			// Now that we have populated the IBO's getter/setter definitions, return
			// the This reference. We are doing this so that the LoadQuery() method can
			// be chained with the IBO's constructor.
			return( THIS );
 
		</cfscript>
	</cffunction>
 
 
	<cffunction name="Next" access="public" returntype="boolean" output="false"
		hint="Increments the IBO cursor to point to the next record in the record set. Returns true if the new iteration is pointing to a valid record.">
 
		<cfscript>
 
			// Increment the iteration index to point to the next record. At this
			// point, we don't care about any sort of validation. That is going
			// to be taken care of in the next step.
			VARIABLES.Instance.IterationIndex = (VARIABLES.Instance.IterationIndex + 1);
 
			// Point the public row to the iteration index.
			THIS.CurrentRow = VARIABLES.Instance.IterationIndex;
 
			// Check to see if the new index points to a valid row. If it does, return true
			// otherwise, return false.
			return( VARIABLES.Instance.IterationIndex LTE VARIABLES.Instance.RecordSet.RecordCount );
 
		</cfscript>
	</cffunction>
 
 
	<cffunction name="Set" access="public" returntype="void" output="false"
		hint="Sets the value at the given key. Throws an error if the key is not available.">
 
		<!--- Define arguments. --->
		<cfargument name="Key" type="string" required="true" />
		<cfargument name="Value" type="any" required="true" />
 
		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />
 
		<!--- Check to see if the key is available. --->
		<cfif StructKeyExists( VARIABLES.Instance.SetAttributes, ARGUMENTS.Key )>
 
			<!--- This key is valid. Check to see how it is being accessed. --->
			<cfif (VARIABLES.Instance.SetAttributes[ ARGUMENTS.Key ] EQ VARIABLES.KeyTypes.PROPERTY)>
 
				<!--- Set the property directly in the record set. --->
				<cfset VARIABLES.Instance.RecordSet[ ARGUMENTS.Key ][ VARIABLES.Instance.IterationIndex ] = ARGUMENTS.Value />
 
			<cfelse>
 
				<!---
					The key is being accessed as a method. Get a pointer to the method.
					We need to get the interim method since ColdFusion cannot handle the
					parsing of an array notation followed by a method call. Furthermore,
					we don't want to use CFInvoke as CFInvoke requires the naming of the
					arguments. By creating a method pointer, we can use positional argument
					mapping, not mappings by name. This makes for a more flexible interface.
				--->
				<cfset LOCAL.Method = VARIABLES.Instance.SetAttributeMethods[ ARGUMENTS.Key ] />
 
				<!--- Call the set method. --->
				<cfset LOCAL.Method( ARGUMENTS.Value ) />
 
			</cfif>
 
 
			<!---
				At this point we have either set the value as a property or by
				method. Eitherway, return out.
			--->
			<cfreturn />
 
		<cfelse>
 
			<!--- The key is not available for setting. Throw an error. --->
			<cfthrow
				message="You have attempted to access an invalid key."
				type="IBO.InvalidSetKey"
				detail="The key you are trying to set, which is currently #UCase( ARGUMENTS.Key )#, is not available in this IBO."
				/>
 
		</cfif>
	</cffunction>
 
</cfcomponent>

For Cut-and-Paste