Using Metro - Decorators and Validation

2009 January 05
tags: ColdFusion · jQuery · Metro · Transfer
by Paul Marcotte

In the first part of Using Metro, I reviewed the Service and Gateway portion of the Metro security package. In this post, I'll describe the core Metro Decorator and Validator as well as the jQuery plugin I use for client side validation.

I'll use the loginUser() method from the concrete SecurityService to demonstrate the interaction between Service, Business Object and Gateway. Here is the complete code for the method.

<cffunction name="loginUser" access="public" output="false" returntype="any">
<cfset var user = new("User") />
<cfset var result = getTransientFactory().create("Result")>
<cfset var errors = StructNew()>
<cfset result.setErrors(user.populate(argumentCollection=arguments))>
<cfif (result.getSuccess())>
<cfset result.setErrors(user.validate("login"))>
<cfif (result.getSuccess())>
<cfset user = getGateway("User").loginUser(user)>
<cfif user.getIsPersisted()>
<cfset setUserSession(user.getUserId())>
<cfelse>
<cfset errors['badCredentials'] = "Username/Password credentials invalid. Please try again.">
<cfset result.setErrors(errors)>
</cfif>
</cfif>
</cfif>
<cfset result.setResult(user)>
<cfreturn result>
</cffunction>

In a nutshell, the loginUser() method populates a new User object and validates the state for the "login" context. If those operations are successful, we pass the user object to the custom Gateway method to retrieve a persisted user object. If the returned user is persisted, we start a session, else we set a simple error message. I added the concrete Gateway and custom method for the sole purpose of demonstrating that you can write concrete gateways. It is not meant to be a best practice of any sort.

Rules and Contexts

The decorator for the User object is "metro.security.User". This decorator extends the metro.core.Decorator which contains populate() and validate() methods. The actual validation for the object is delegated to the generic metro.core.Validator which provides some simple rules based validation based on metadata I create in the configure method of the decorator. Here's a truncated version of the configure() method.

<cffunction name="configure" access="private" output="false" returntype="void">
<cfscript>
variables.rules = StructNew();

addRule(property="Username",
label="Username",
datatype="string",
required=true,
min=6,
max=16);

addRule(property="Password",
label="Password",
datatype="string",
required=true,
min=6,
max=32);

variables.context = StructNew();
variables.context["login"] = "Username,Password";
</cfscript>
</cffunction>

I define a rule for each property with addRule() and then create contexts for standard actions like "login", "save", "register". So, looking at the rules as described above, Username is required, must be a valid string between 6 and 16 characters in length. And Password is required and must be a valid string between 6 and 32 characters.

Populate and Validate

The populate() method looks for any matching setters in the object for the keys in the populate() argumentCollection. Populate() returns a structure of errors if any datatype is incorrect, for a matching key/setter.

<cffunction name="populate" access="public" returntype="struct" output="false">
<cfset var argTypes = getMethodArgumentTypes()>
<cfset var i = "">
<cfset var errors = StructNew()>
<cfset var message = "">
<cfloop collection="#arguments#" item="i">
<cfif (StructKeyExists(this,"set"&i))>
<cfif (IsValid(argTypes[i],arguments[i]))>
<cfinvoke method="set#i#">
<cfinvokeargument name="#i#" value="#arguments[i]#" />
</cfinvoke>
<cfelse>
<cfset message = "The value '"& arguments[i] & "' for " & i & " is not of type " & argTypes[i]>
<cfset StructInsert(errors,i,message)>
</cfif>
</cfif>
</cfloop>
<cfset setIsDirty(true)>
<cfreturn errors>
</cffunction>

Validate() also returns a struct of errors if the object does not pass the validation rules for the context passed in.

<cffunction name="validate" access="public" output="false" returntype="struct">
<cfargument name="context" type="string" required="true">
<cfreturn getValidator().validate(this,arguments.context) />
</cffunction>

A little jQuery on the client side

I've become a big fan of jQuery over the past year, primarily for the flexibility and extensibility. One of the goals I had in developing my own DSL for object validation was to be able to reuse those rules within a jQuery plugin. Metro includes the jquery.formrules.js plugin which provides blur and submit validation for forms based on the rules returned for a specific context. Here's a sample of how I incorporate this in a login view.

<cfsilent>
<cfset currentUser = event.getValue("currentUser")>
<cfset rules = currentUser.getRules("login")>
</cfsilent>
<cfoutput>
<cfif StructCount(rules)>
<script type="text/javascript" src="/metro/js/jquery.formrules.js"></script>
<script type="text/javascript">
var jsonrules = #serializejson(rules)#
jQuery(function() {
jQuery("##login_form").formrules(jsonrules);
});
</script>
</cfif>
<div id="login">
<form name="login_form" id="login_form" action="/security/doLogin" method="post">
&amp;nbsp; <fieldset>
<legend>Login</legend>
<div class="formitem">
<label class="inline" for="Username">Username</label>
<input name="Username" id="Username" type="text" class="text" value="#currentUser.getUsername()#" />
</div>
<div class="formitem">
<label class="inline" for="Password">Password</label>
<input name="Password" id="Password" type="password" class="text" value="#currentUser.getPassword()#" />
</div>
<div class="inline">&amp;nbsp;</div><button type="submit" id="submit">Login</button>
</fieldset>
</form>
</div>
</cfoutput>

Hopefully, these pieces make some sense for anyone looking to incorporate Metro into their application development. I'll have some more examples ready in the near future.