Aliaspooryorik
ColdFusion ORM Book

Using Paul Marcotte's Metro

I have been trying out Paul Marcotte's Metro which is available from http://metro.riaforge.org/. In Paul's words; "Metro is a library of components to support rapid development of applications that use ColdSpring and Transfer ORM." To me it is more than this as it also includes a bunch of user security and validation functionality. Everything is organised as one service and one gateway per package (the packages are defined in the Transfer config file). A package can have one or more objects.

There are a few MXUnit tests included in the download, but no examples of how to use it (this is not a critisism!) As a result I decided to see how I could incorporate it into an application.

Metro is designed to be extended, so that's what I did! Metro uses Convention over Configuration and will look for classes named after the Transfer packages in the following locations:

  1. A Service and/or Gateway(s) found within the relative path "/{componentPath}/{packageName}".
  2. A Service and/or Gateway(s) found within the relative path "/{libPath}/{packageName}".
  3. A Service and/or Gateway(s) found within the relative path "/metro/{packageName}".
  4. The Service and Gateway found in "/metro/core".

The component path is defined in the ColdSpring config file, so can be anywhere. I haven't tried using the libPath param. I used the Transfer and ColdSpring config files included it the tests folder for my sample app to keep things simple.

The first thing I did was to create a subfolder inside the default metro install directory called "sampleapp". The files in the "sampleapp" directory are:

  • Application.cfc
  • list.cfm
  • edit.cfm
  • add.cfm

I created an empty securityService.cfc class (security is the name of the Transfer package) in my model directory (defined by the {componentPath}) which simply extends metro.security.SecurityService. This is my concrete securityService class, where I can add my own methods if I want without having to worry about a new version of Metro being released and overwriting my changes. It is worth noting that I didn't need to update the ColdSpring config to use this new concrete service.

Metro makes clever use of onMissingMethod to not only get and set values, but also derives the class name from the method name. This caught me out for a bit as the downside to using onMissingMethod is that there is no complete API. However once you realise this then it is a piece of cake to use.

The security package includes four objects:

  • Audit
  • User
  • Role
  • Permission

To call any methods on these objects you simple call a function named 'type of operation', 'object name'. For example for the User object you can do:

  • getUser() : returns a populated Transfer User object (by id)
  • listUser() : returns a query object
  • newUser() : returns a blank Transfer User object
  • saveUser() : returns a metro.util.Result
  • deleteUser() : void

I also added a new method called "array" to the metro.core.Service class to return an array of Transfer objects, so that I can take advantage of Transfer's decorators, but I'll ignore that for now as it is not part of Metro 0.3.2.

The observant among you may have noticed that saveUser returns a metro.util.Result object. This is a really neat feature where validation is handled. What Metro does is to create a clone of your object when it tries to save the values you've passed it. Why does it do this? Well, ideally you don't want your object to contain invalid data, so one solution is to create a clone of your object and populate that with the data and then check to see if it is valid before updating your object. That way, your object is protected. Metro also allows you to access the clone instance as well as the 'proper' object. This is really handy if your want to fill in a form with the invalid information the user has entered, along with any any messages.

Anyway enough talk here is my code:

Application.cfc


<cfcomponent>

<cfset this.name = Hash(getDirectoryFromPath(getCurrentTemplatePath())) />

<cffunction name="onApplicationStart"
returntype="boolean"
output="false"
access="public"
hint="I am executed when the application starts">


<cfscript>
var SecurityService = "";

var pathToRoot = ReReplaceNoCase(CGI.SCRIPT_NAME, "/[^/]{1,}\.cfm(.){0,}", "/", "one");
// coldspring config
var beanDefs = expandPath(pathToRoot & 'config/Coldspring.xml');
var params = StructNew();
// transfer
params.datasourcePath = pathToRoot & 'config/Datasource.xml';
params.transferConfigPath = pathToRoot & 'config/Transfer.xml';
params.definitionsPath = pathToRoot & 'config/definitions';
// metro model component root
params.componentPath = "metro/sampleapp/com/domain/model";

Application.BeanFactory = CreateObject('component', 'coldspring.beans.DefaultXmlBeanFactory').init(StructNew(),params);
Application.BeanFactory.loadBeans( beanDefs );


// create a default user for testing against
SecurityService = Application.BeanFactory.GetBean ( "SecurityService" );

if ( SecurityService.listUser().RecordCount eq 0 ) {

params = StructNew();
params.UserId = 0;
params.RoleId = 1;
params.FirstName = "Test";
params.LastName = "Account";
params.Username = "tester";
params.Password = "tester";
params.ConfirmPassword = "tester";
params.Email = "foo@domain.com";

SecurityService.saveUser( argumentCollection=params );
}
</cfscript>

<cfreturn true />
</cffunction>

<cffunction name="onRequestStart"
returntype="boolean"
output="false"
access="public"
hint="I am executed at the start of each request">

<cfargument name="targetPage"
type="string"
required="true" />

<!--- as I'm testing call reset the application on each test --->
<cfset this.onApplicationStart() />

<cfreturn true />
</cffunction>

<cffunction name="onRequestEnd"
returntype="void"
output="false"
access="public"
hint="I am executed at the end of each request">

<cfargument name="targetPage" type="string" required="true" />
</cffunction>

<cffunction name="onError"
returntype="void"
output="true"
access="public"
hint="I am executed when an unhandled error occurs">

<cfargument name="exception"
type="any"
required="true" />

<cfargument name="eventName"
type="string"
required="true" />

<cfdump var="#arguments# />
</cffunction>

</cfcomponent>

list.cfm


<cfset UserService = Application.BeanFactory.getBean( "SecurityService" ) />
<cfif StructKeyExists( URL, "delete_userid" )>
<cfset UserService.deleteUser( URL.delete_userid ) />
</cfif>
<cfset Users = UserService.arrayUser() />

<cfoutput>
<p><a href="add.cfm">[add]</a></p>

<table border="1">
<tr>
<td>UserID</td>
<td>Email</td>
<td>Password Hash</td>
<td>Role</td>
<td>Delete</td>
<td>Edit</td>
</tr>
<cfloop array="#Users#" index="user">
<tr>
<td>#user.getUserID()#</td>
<td>#user.getEmail()#</td>
<td>#user.getPassword()#</td>
<td>#user.getRole().getDescription()#</td>
<td><a href="list.cfm?delete_userid=#user.getUserID()#">[delete]</a></td>
<td><a href="edit.cfm?userid=#user.getUserID()#">[edit]</a></td>
</tr>
</cfloop>
</table>
</cfoutput>

edit.cfm


<cfset UserService = Application.BeanFactory.getBean( "SecurityService" ) />
<cfset User = UserService.getUser( URL.userid ) />

<cfif StructCount( form ) neq 0>
<!--- update user --->

<cfset args = {
Email = form.email,
Firstname = form.forename,
Lastname = form.surname,
Username = form.username,
Active = form.active,
UserID = user.getUserID(),
RoleID = form.role
} /
>


<cfif Trim( form.password ) neq "">
<cfset args.Password = form.password />
</cfif>

<cfset result = UserService.saveUser( argumentCollection=args ) />

<cfif !result.getSuccess()>
<!---
there are errors
--->

<cfset Errors = result.getErrors() />
<!---
we need to get the cloned object that contains the invalid data to show on the form.
Note that this is not the persisted data.
--->

<cfset User = result.getResult() />

<cfelse>
<!--- all good --->
<cflocation addtoken="false" url="list.cfm" />
</cfif>

</cfif>

<cfset Roles = UserService.listRole() />


<cfoutput>

<cfif IsDefined("Errors")>
<ul>
<cfloop collection="#Errors#" item="error">
<li>#Errors[ error ]#</li>
</cfloop>
</ul>
</cfif>

<form action="edit.cfm?userid=#user.getUserID()#" method="post">
<table border="1">
<tr>
<td>ID</td>
<td>#user.getUserID()#</td>
</tr>
<tr>
<td>Email</td>
<td><input type="text" name="email" id="email" value="#user.getEmail()#" /></td>
</tr>
<tr>
<td>New Password</td>
<td><input type="text" name="password" id="password" value="" /></td>
</tr>
<tr>
<td>First name</td>
<td><input type="text" name="forename" id="forename" value="#user.getFirstname()#" /></td>
</tr>
<tr>
<td>Last name</td>
<td><input type="text" name="surname" id="surname" value="#user.getLastname()#" /></td>
</tr>
<tr>
<td>Username</td>
<td><input type="text" name="username" id="username" value="#user.getUsername()#" /></td>
</tr>
<tr>
<td>Active</td>
<td><input type="text" name="active" id="active" value="#user.getActive()#" /></td>
</tr>
<tr>
<td>Roles</td>
<td>
<cfloop query="Roles">
<input type="radio" name="role" id="role_#Roles.roleid#" value="#Roles.roleid#" <cfif user.getRole().getRoleId() EQ Roles.roleid>checked="checked"</cfif> />#Roles.description#<br />
</cfloop>
</td>
</tr>
<tr>
<td> </td>
<td><input type="submit" name="save" id="save" value="save" /></td>
</tr>
</table>
</form>

</cfoutput>

add.cfm


<cfset UserService = Application.BeanFactory.getBean( "SecurityService" ) />

<cfif StructCount( form ) neq 0>
<!--- update user --->
<cfset User = UserService.getUser( URL.userid ) />

<cfset args = {
Email = form.email,
Password = form.password,
Firstname = form.forename,
Lastname = form.surname,
Username = form.username,
Active = form.active,
RoleID = form.role,
UserID = user.getUserID()
} /
>


<cfset result = UserService.saveUser( argumentCollection=args ) />

<cfif !result.getSuccess()>
<!---
there are errors
--->

<cfset Errors = result.getErrors() />
<!---
we need to get the cloned object that contains the invalid data to show on the form.
Note that this is not the persisted data.
--->

<cfset User = result.getResult() />

<cfelse>
<!--- all good --->
<cflocation addtoken="false" url="list.cfm" />
</cfif>

<cfelse>
<cfset User = UserService.newUser( ) />
</cfif>

<cfset Roles = UserService.listRole() />

<cfoutput>

<cfif IsDefined("Errors")>
<ul>
<cfloop collection="#Errors#" item="error">
<li>#Errors[ error ]#</li>
</cfloop>
</ul>
</cfif>

<form action="edit.cfm?userid=#user.getUserID()#" method="post">
<table border="1">
<tr>
<td>ID</td>
<td>#user.getUserID()#</td>
</tr>
<tr>
<td>Email</td>
<td><input type="text" name="email" id="email" value="#user.getEmail()#" /></td>
</tr>
<tr>
<td>Password</td>
<td><input type="text" name="password" id="password" value="" /></td>
</tr>
<tr>
<td>First name</td>
<td><input type="text" name="forename" id="forename" value="#user.getFirstname()#" /></td>
</tr>
<tr>
<td>Last name</td>
<td><input type="text" name="surname" id="surname" value="#user.getLastname()#" /></td>
</tr>
<tr>
<td>Username</td>
<td><input type="text" name="username" id="username" value="#user.getUsername()#" /></td>
</tr>
<tr>
<td>Active</td>
<td><input type="text" name="active" id="active" value="#user.getActive()#" /></td>
</tr>
<tr>
<td>Roles</td>
<td>
<cfloop query="Roles">
<input type="radio" name="role" id="role_#Roles.roleid#" value="#Roles.roleid#" />#Roles.description#<br />
</cfloop>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td><input type="submit" name="save" id="save" value="save" /></td>
</tr>
</table>
</form>

</cfoutput>

This is by no means a comprehensive example of what Metro can do, but along with the MXUnit tests included in the download hopefully it has piqued your interest.

Nice work Paul!


2 comments

  1. Hi John,

    Great example code. One clarification regarding the conventions for Metro. The ServiceFactory will return one Service per package, composed of one Gateway for each object in that package. This allows one to write a concrete Gateway specific to any object in the package provided that an additional metadata attribute (objectname="Role" for example) be added to component tag.

    Also, it was my intent to create Metro with extensibility in mind, so that you could define your own library of components that themselves extend the Metro core components. In this way, you could implement your array() method and override the onMissingMethod to enhance the functionality without having to modify the core components upon subsequent releases. Unfortunately, you cannot do this and extend the SecurytService in the the security package at the same time. However, you could create a security package within your custom library, so that the SecurityService extends your custom Service.

    Again, thanks for the example code!

    Comment by Paul Marcotte – January 13, 2009
  2. Hi Paul,

    Ah I see, I hadn't realised that it is one gateway per object and one service per package. Good to know. I haven't tried writing my own concrete Gateway as Metro already does what I wanted to do.

    I'll have to look into using the library, I take it the library classes extend all subclasses and don't apply to a specific package or service.

    Comment by John Whish – January 13, 2009

Leave a comment

If you found this post useful, interesting or just plain wrong, let me know - I like feedback :)

Please note: If you haven't commented before, then your comments will be moderated before they are displayed.

Please subscribe me to any further comments
 

Search

Wish List

Found something helpful & want to say ’thanks‘? Then visit my Amazon Wish List :)

Categories

Recent Posts