Gregg is Co-founder and CTO of Wild Bamboo Rocket, LLC. He lives in Wichita, KS with his wife, 3 kids, a cat and a dog. Gregg has posted 6 posts at DZone. View Full User Profile

Using Groovy to Generate Objective-C

02.05.2010
| 9520 views |
  • submit to reddit

I'm in the process of porting a Windows Mobile application to the iPhone for a local company.  The existing Windows Mobile application uses a local database to store everything and then there is a manual sync process when the users have some kind of connectivity.  The data is then transmitted to the mother ship and updates and new data are pulled down to the device.  The tables are not relational at all.  Data is duplicated where possible and each table, more or less, represents a form.

Due to time constraints and taking into consideration the company is taking over the development once I deliver version 1.0, I decided trying to make this model better wasn't feasible.  Taking a similar approach was the quickest solution and something I believed the other developers would be able to understand.

 So long story short I needed a bunch of classes that represented tables.  These classes would also follow an Active Record pattern in that data access would be accomlished through instances of these classes and they would act directly on their respective tables in the database.  After creating about two of these classes (both header file and class file) I looked for a better way.  I needed about 40 of these and I was already tired of typing and making a lot of silly mistakes.  Enter Groovy's SimpleTemplateEngine.

First, let's look at what I needed to generate.  The header file:

#import <Foundation/Foundation.h>
#import "BaseDataObj.h"

@interface User : BaseDataObj {

NSString *userId;
NSString *password;
NSString *userType;
NSString *prefix;
NSString *firstName;
NSString *lastName;
NSString *middleName;
NSString *suffix;
NSString *address;
NSString *address2;
NSString *city;
NSString *state;
NSString *zipCode;
NSString *sex;
NSString *dateOfBirth;
NSString *emailAddress;
NSString *staffNo;
NSString *useAuthentic;
NSString *neworkName;
NSString *networkDomain;
NSString *sProviderId;
NSString *wlkstGrp;

}

@property (nonatomic, retain) NSString *userId;
@property (nonatomic, retain) NSString *password;
@property (nonatomic, retain) NSString *userType;
@property (nonatomic, retain) NSString *prefix;
@property (nonatomic, retain) NSString *firstName;
@property (nonatomic, retain) NSString *lastName;
@property (nonatomic, retain) NSString *middleName;
@property (nonatomic, retain) NSString *suffix;
@property (nonatomic, retain) NSString *address;
@property (nonatomic, retain) NSString *address2;
@property (nonatomic, retain) NSString *city;
@property (nonatomic, retain) NSString *state;
@property (nonatomic, retain) NSString *zipCode;
@property (nonatomic, retain) NSString *sex;
@property (nonatomic, retain) NSString *dateOfBirth;
@property (nonatomic, retain) NSString *emailAddress;
@property (nonatomic, retain) NSString *staffNo;
@property (nonatomic, retain) NSString *useAuthentic;
@property (nonatomic, retain) NSString *neworkName;
@property (nonatomic, retain) NSString *networkDomain;
@property (nonatomic, retain) NSString *sProviderId;
@property (nonatomic, retain) NSString *wlkstGrp;

@end

And the class file:

#import "User.h"
#import "DBManager.h"

@implementation User
@synthesize userId;
@synthesize password;
@synthesize userType;
@synthesize prefix;
@synthesize firstName;
@synthesize lastName;
@synthesize middleName;
@synthesize suffix;
@synthesize address;
@synthesize address2;
@synthesize city;
@synthesize state;
@synthesize zipCode;
@synthesize sex;
@synthesize dateOfBirth;
@synthesize emailAddress;
@synthesize staffNo;
@synthesize useAuthentic;
@synthesize neworkName;
@synthesize networkDomain;
@synthesize sProviderId;
@synthesize wlkstGrp;

-(id)init {

if (self = [super init]) {
}
return self;
}

#pragma mark NSCoding
-(void) encodeWithCoder:(NSCoder *) encoder {
[encoder encodeObject:userId forKey:@"userId"];
[encoder encodeObject:password forKey:@"password"];
[encoder encodeObject:userType forKey:@"userType"];
[encoder encodeObject:prefix forKey:@"prefix"];
[encoder encodeObject:firstName forKey:@"firstName"];
[encoder encodeObject:lastName forKey:@"lastName"];
[encoder encodeObject:middleName forKey:@"middleName"];
[encoder encodeObject:suffix forKey:@"suffix"];
[encoder encodeObject:address forKey:@"address"];
[encoder encodeObject:address2 forKey:@"address2"];
[encoder encodeObject:city forKey:@"city"];
[encoder encodeObject:state forKey:@"state"];
[encoder encodeObject:zipCode forKey:@"zipCode"];
[encoder encodeObject:sex forKey:@"sex"];
[encoder encodeObject:dateOfBirth forKey:@"dateOfBirth"];
[encoder encodeObject:emailAddress forKey:@"emailAddress"];
[encoder encodeObject:staffNo forKey:@"staffNo"];
[encoder encodeObject:useAuthentic forKey:@"useAuthentic"];
[encoder encodeObject:neworkName forKey:@"neworkName"];
[encoder encodeObject:networkDomain forKey:@"networkDomain"];
[encoder encodeObject:sProviderId forKey:@"sProviderId"];
[encoder encodeObject:wlkstGrp forKey:@"wlkstGrp"];

}

-(id) initWithCoder:(NSCoder *) decoder {
if (self = [super init]) {
self.userId = [decoder decodeObjectForKey:@"userId"];
self.password = [decoder decodeObjectForKey:@"password"];
self.userType = [decoder decodeObjectForKey:@"userType"];
self.prefix = [decoder decodeObjectForKey:@"prefix"];
self.firstName = [decoder decodeObjectForKey:@"firstName"];
self.lastName = [decoder decodeObjectForKey:@"lastName"];
self.middleName = [decoder decodeObjectForKey:@"middleName"];
self.suffix = [decoder decodeObjectForKey:@"suffix"];
self.address = [decoder decodeObjectForKey:@"address"];
self.address2 = [decoder decodeObjectForKey:@"address2"];
self.city = [decoder decodeObjectForKey:@"city"];
self.state = [decoder decodeObjectForKey:@"state"];
self.zipCode = [decoder decodeObjectForKey:@"zipCode"];
self.sex = [decoder decodeObjectForKey:@"sex"];
self.dateOfBirth = [decoder decodeObjectForKey:@"dateOfBirth"];
self.emailAddress = [decoder decodeObjectForKey:@"emailAddress"];
self.staffNo = [decoder decodeObjectForKey:@"staffNo"];
self.useAuthentic = [decoder decodeObjectForKey:@"useAuthentic"];
self.neworkName = [decoder decodeObjectForKey:@"neworkName"];
self.networkDomain = [decoder decodeObjectForKey:@"networkDomain"];
self.sProviderId = [decoder decodeObjectForKey:@"sProviderId"];
self.wlkstGrp = [decoder decodeObjectForKey:@"wlkstGrp"];
}
return self;
}

#pragma mark -
#pragma mark NSCopying
-(id) copyWithZone:(NSZone *) zone {
User *copy = [[[self class] allocWithZone: zone] init];
userId = [self.userId copy];
password = [self.password copy];
userType = [self.userType copy];
prefix = [self.prefix copy];
firstName = [self.firstName copy];
lastName = [self.lastName copy];
middleName = [self.middleName copy];
suffix = [self.suffix copy];
address = [self.address copy];
address2 = [self.address2 copy];
city = [self.city copy];
state = [self.state copy];
zipCode = [self.zipCode copy];
sex = [self.sex copy];
dateOfBirth = [self.dateOfBirth copy];
emailAddress = [self.emailAddress copy];
staffNo = [self.staffNo copy];
useAuthentic = [self.useAuthentic copy];
neworkName = [self.neworkName copy];
networkDomain = [self.networkDomain copy];
sProviderId = [self.sProviderId copy];
wlkstGrp = [self.wlkstGrp copy];

return copy;
}

- (void)dealloc {

[userId release];
[password release];
[userType release];
[prefix release];
[firstName release];
[lastName release];
[middleName release];
[suffix release];
[address release];
[address2 release];
[city release];
[state release];
[zipCode release];
[sex release];
[dateOfBirth release];
[emailAddress release];
[staffNo release];
[useAuthentic release];
[neworkName release];
[networkDomain release];
[sProviderId release];
[wlkstGrp release];

[super dealloc];
}

@end

 I think you'll see why writing 40 of these will require me to seek psychological help.  XCode, to my knowledge, won't help me here either.  The first thing I had to do was determine how I was going to specify all the properties.  Something overly verbose would defeat the purpose.  I decided to use ConfigSlurper to read a configuration file of all my properties.  Here is what this file looks like

descriptions {
USERS {
objName = "User"
columns {
USER_ID = "text"
PASSWORD = "text"
USER_TYPE = "text"
PREFIX = "text"
FIRST_NAME = "text"
LAST_NAME = "text"
MIDDLE_NAME = "text"
SUFFIX = "text"
ADDRESS = "text"
ADDRESS_2 = "text"
CITY = "text"
STATE = "text"
ZIP_CODE = "text"
SEX = "text"
DATE_OF_BIRTH = "text"
EMAIL_ADDRESS = "text"
STAFF_NO = "text"
USE_AUTHENTIC = "text"
NEWORK_NAME = "text"
NETWORK_DOMAIN = "text"
S_PROVIDER_ID = "text"
WLKST_GRP = "text"
}
}
}

Note that for each table in the database, another section would follow after 'USER'.  For reasons I'm choosing not to explain in this article the properties in the config file represent the table name, column names, and column types.  Reading this configuration is quite simple:

// an array of all the tables I need to read
def tables = ['USERS']

// TableDescriptions is the name of the config file
def configObject = new ConfigSlurper().parse(TableDescriptions)

tables.each() {
// get the column data for this table
def columnData = configObject.descriptions."${it}".columns
// do something with it
}

The question now is, what are we doing with this data?  The first thing I needed to do was convert a property like NETWORK_NAME to networkName.

def camalCase(key) {
def bits = key.split('_')
def newKey = ""
bits.eachWithIndex() {obj, index ->
if (index == 0) {
newKey += obj.toLowerCase()
}else{
def fLetter = obj.substring(0,1)
newKey += fLetter.toUpperCase()
newKey += obj.substring(1, obj.length()).toLowerCase()
}
}
return newKey
}

I can now use this method as well as some other code to generate my header and class files.  Each .h and .m file have a template that is used by SimpleTemplateEngine.

#import <Foundation/Foundation.h>
#import "BaseDataObj.h"

@interface $className : BaseDataObj {

$variables
}

$properties

@end
#import "$className.h"
#import "DBManager.h"

@implementation $className
$synthesizers

-(id)init {
return self;
}

#pragma mark NSCoding
-(void) encodeWithCoder:(NSCoder *) encoder {
$encoders
}

-(id) initWithCoder:(NSCoder *) decoder {
if (self = [super init]) {
$decoders
}
return self;
}

#pragma mark -
#pragma mark NSCopying
-(id) copyWithZone:(NSZone *) zone {
$className *copy = [[[self class] allocWithZone: zone] init];
$zoneCopies
return copy;
}

- (void)dealloc {
$deallocs
[super dealloc];
}

@end

With those templates in place I can begin generating the code that will replace the $variables in the templates.

tables.each() {
def objName = configObject.descriptions."${it}".objName
def table = "${it}"
def columnData = configObject.descriptions."${it}".columns
createHeaderFromTemplate(objName, columnData)
createImplFromTemplate(objName, table, columnData)
}

def createHeaderFromTemplate(objName, columnData){

def variables = ""
columnData.each() { key, value ->
def newKey = camalCase(key)
if (value.equals('text')) {
variables += "NSString *${newKey};\n\t"
}else if (value.equals('integer')) {
variables += "NSInteger ${newKey};\n\t"
}
}

def properties = ""

columnData.each() { key, value ->
def newKey = camalCase(key)
if (value.equals('text')) {
properties += "@property (nonatomic, retain) NSString *${newKey};\n"
}else{
properties += "@property NSInteger ${newKey};\n"
}
}

def binding = ['className':objName, 'variables':variables, 'properties':properties]
def template = new File('templates/header.h').getText()

def engine = new SimpleTemplateEngine()
def output = engine.createTemplate(template).make(binding)

new File("data/${objName}.h").text = output

}

I create a String for each variable in the template that will be replaced and loop over the column data to generate all the code.  I then create a map of bindings, get the contents of my template file, instantiate a new SimpleTemplateEngine and let it do all the find/replace work.  Then I just save the output to a file, appropriately named, based on the data from ConfigSlurper.

This code generates the header file.  Similar code is used to generate the class file.  I've omitted it for brevity.  On the surface this looks like a lot of code to generate code.  But the end result is I've got a framework in place for any template I need and I save myself a ton of typing (remember, 40+ files to generate, some with 30+ properties) and a lot of typographical mistakes.  All this made possible by the awesomeness of Groovy's ConfigSlurper and SimpleTemplateEngine.

Published at DZone with permission of its author, Gregg Bolinger.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Nick Kijak replied on Fri, 2010/02/05 - 2:02pm

I have never used it or really looked into it, but isn't this what Core Data is for?

Thomas Quintana replied on Fri, 2010/02/05 - 3:18pm

I suggest you look in to XML VM I haven't used it except for some trivial testing but it shows a lot of promise.

 

I hope it helps :-)

 

Cheers,

Thomas

Gregg Bolinger replied on Fri, 2010/02/05 - 3:25pm

@prodrive555 - assuming you are using CDS, sure. But if you aren't, and I'm not, this is another approach. BTW, this is more about using Groovy's awesomeness to generate code rather than what I am actually generating. I just used this as an example.

Nick Kijak replied on Fri, 2010/02/05 - 5:56pm in response to: Gregg Bolinger

Yes, sorry. I didn't mean to take away from that. It is a good use of the power of groovy. I was (poorly) asking a general question.

Mark Bednarczyk replied on Sun, 2010/02/07 - 11:12am

Another alternative for repetitive template code like this is ANTLR's string templates: http://www.stringtemplate.org. Cheers, mark...

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.