Blog

CommandBox - Commands can be Clean Code Too

Gavin Pickin February 17, 2015

Spread the word

Gavin Pickin

February 17, 2015

Spread the word


Share your thoughts

In today's world, we use frameworks, and separate concerns with MVC or MV*, we strive for Clean Code to make Uncle Bob proud, or Bossman Bob at least. Just because the “Run” function is all you need in a CFC (which extends the BaseCommand) for CommandBox to index the Command, doesn’t mean you have to jam all your code into that one function. Lets look at some simple code cleanup.

On my own blog, we've looked a creating a simple set of commands, which I decided to call KIWI SAYS, which help with creating Host files in Apache, Lucee, and gives you some shortcuts for restarting your engines, but the main command in the set is addWebsite. It is the main controller almost, that does a lot of checking config, and setup, and if everything is good, then we’ll actually do produce the output, and restart apache. It seems like a perfect place to start, because that's a lot of code.

Assuming I had done no cleanup or refactoring, my run function would look something like this.

component extends="commandbox.system.BaseCommand" {
   
    property name="settingsObj";
        
    /**
* @websitePath.hint Path to the Website Directory - .\ for current directory
* @websiteURL.hint The Website URL
* @engine.hint The CFML Engine you want the Website to run on - lucee, railo or cf11
* @engine.options Lucee,Railo,CF11
*/
    function run( required string websitePath, required string websiteURL, required string engine ){
        this.settingsObj = createObject( "component", "set.settings");
       
        ARGUMENTS.websitePath =  fileSystemUtil.resolvePath( ARGUMENTS.websitePath );
        if( !directoryExists( ARGUMENTS.websitePath ) ) {
            return error( "#ARGUMENTS.websitePath#: No such directory - Please enter a valid relative or full path" );
        }
        if ( listFind( "lucee,railo,cf11", ARGUMENTS.engine) == false ){
               error('Please select a valid Engine - Lucee, Railo or CF11’);
        }
        var apacheConf = "";
        apacheConf = apacheConf & '<VirtualHost *:80>' & CR;
        if ( this.settingsObj.get("ApacheAdminEmail") GT ""){
            apacheConf = apacheConf & 'ServerAdmin ' & this.settingsObj.get("ApacheAdminEmail") & CR;
        }
        apacheConf = apacheConf & 'DocumentRoot "#websitePath#"' & CR;
        apacheConf = apacheConf & 'ServerName #websiteURL#' & CR;
        if ( engine == 'CF11'){
            apacheConf = apacheConf & & 'Include "' & this.settingsObj.get("cf11ConnFile") & '"' & CR;
        }
        else {
            apacheConf = apacheConf  & '<Proxy *>' & CR;
            apacheConf = apacheConf & 'Allow from 127.0.0.1' & CR;
            apacheConf = apacheConf & '</Proxy>' & CR;
            apacheConf = apacheConf & 'ProxyPreserveHost On' & CR;
            apacheConf = apacheConf & 'ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:';

            if ( engine == "Railo" ){
                  apacheConf = apacheConf & this.settingsObj.get("railoAJPPort");
            }
            else {
                  apacheConf = apacheConf & this.settingsObj.get("luceeAJPPort");
            }
            apacheConf = apacheConf &  replacenocase( websitePath, this.settingsObj.get("tomcatRootPath"), "" );
            apacheConf = apacheConf & '/$1$2' & CR;
        }
        apacheConf = apacheConf & '</VirtualHost>' & CR;
        print.line().blueLine( apacheConf );
        
        var apacheConfPath = this.settingsObj.get("apacheConfPath");
        print.line( 'apacheConfPath = ' & apacheConfPath );
        if( apacheConfPath == '' ) {
            return error( "#apacheConfPath#: No such directory" );
        }
   
        apacheConfPath = fileSystemUtil.resolvePath( apacheConfPath );
        print.line( 'apacheConfPath = ' & apacheConfPath );
        if( !directoryExists( apacheConfPath ) ) {
            return error( "#apacheConfPath#: No such directory" );
        }
        print.line( 'Writing Conf to File: ' & apacheConfPath & '/' & websiteURL & '.conf' );
        fileWrite( apacheConfPath & '/' & websiteURL & '.conf', apacheConf );
        
         runCommand( "kiwiSays restartApache" );
        
    }
}

 

Can you follow that code?

I wrote it, and to be honest, in this format, its not pretty, and I would not like working on this. Lets make it CLEAN(er) code… and we’ll do that like we would do with any other CFC, refactor a piece out into smaller functions. CommandBox doesn’t limited the number of functions you can have in a CFC, it will only index, and run the “RUN” function when the name of the CFC is typed into the console. Lets break this massive run function into a more management controller.

Note: I could actually add additional CFCs, like Services, and call each one to perform functions, but to make it easier for readers, all the functions I refactor are going into the same main CFC. The only exception, if my settingsObj.cfc which I use to wrap input and output to a json file, to store settings for this suite of tools… so you can set your paths for where CF is installed, where Lucee is installed, what ports etc.

First piece of code

BEFORE:

ARGUMENTS.websitePath =  fileSystemUtil.resolvePath( ARGUMENTS.websitePath );
if( !directoryExists( ARGUMENTS.websitePath ) ) {
        return error( "#ARGUMENTS.websitePath#: No such directory - Please enter a valid relative or full path" );
}

We are validating the WebsitePath, so that sounds like a great function name.

AFTER:

ARGUMENTS.websitePath = validateWebsitePath( ARGUMENTS.websitePath );


NEW FUNCTION:

function validateWebsitePath( websitePath ){
   
    websitePath = fileSystemUtil.resolvePath( ARGUMENTS.websitePath );
    if( !directoryExists( ARGUMENTS.websitePath ) ) { 
        error( "#ARGUMENTS.websitePath#: No such directory - Please enter a valid relative or full path" );
    }
    return websitePath;
   
}



Second piece of code

BEFORE:

if ( listFind( "lucee,railo,cf11", ARGUMENTS.engine) == false ){
        error('Please select a valid Engine - Lucee, Railo or CF11’);
}

We are validating required settings, again a great name, so we’re going to replace this with a new function. The best thing about this, I have a lot more settings I want to validate, so I’m going to add more here, and we do not need to worry about the main function getting more polluted.

AFTER:

validateRequiredSettings( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );

 

NEW FUNCTION:

function validateRequiredSettings( required string websiteURL, required string websitePath, required string engine ){
   
    if ( validateEngine( ARGUMENTS.engine ) != ""){
        return error( validateEngine( ARGUMENTS.engine ) );
   
    }
    return "";
   
}

 

Third piece of code

So far, we only saved ourselves a few lines, the next chunk of code, is where you see the real benefits.

BEFORE:

var apacheConf = "";
   
apacheConf = apacheConf & '<VirtualHost *:80>' & CR;
if ( this.settingsObj.get("ApacheAdminEmail") GT ""){
    apacheConf = apacheConf & 'ServerAdmin ' & this.settingsObj.get("ApacheAdminEmail") & CR;
}
apacheConf = apacheConf & 'DocumentRoot "#websitePath#"' & CR;
apacheConf = apacheConf & 'ServerName #websiteURL#' & CR;
if ( engine == 'CF11'){
    apacheConf = apacheConf & & 'Include "' & this.settingsObj.get("cf11ConnFile") & '"' & CR;
}
else {
    apacheConf = apacheConf  & '<Proxy *>' & CR;
    apacheConf = apacheConf & 'Allow from 127.0.0.1' & CR;
    apacheConf = apacheConf & '</Proxy>' & CR;
    apacheConf = apacheConf & 'ProxyPreserveHost On' & CR;
    apacheConf = apacheConf & 'ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:';
    if ( engine == "Railo" ){
        apacheConf = apacheConf & this.settingsObj.get("railoAJPPort");
    }
    else {
        apacheConf = apacheConf & this.settingsObj.get("luceeAJPPort");
    }
    apacheConf = apacheConf &  replacenocase( websitePath, this.settingsObj.get("tomcatRootPath"), "" ) & '/$1$2' & CR;
}
apacheConf = apacheConf & '</VirtualHost>' & CR;
print.line().blueLine( apacheConf );

 

We are generating Apache conf, essentially, although this one piece of code does a lot of smaller tasks inside of it… so we’re first going to refactor it into 1 function, called generateApacheConf surprisingly enough… then we can refactor those other pieces out again.

AFTER:

var apacheConf = generateApacheConf( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );

Only 1 line, now our Run function is looking much better. Lets look at the generateApacheConf function.

NEW FUNCTION:

function generateApacheConf( required string websiteURL, required string websitePath, required string engine ){
   
    var apacheConf = "";
   
    apacheConf = apacheConf & '<VirtualHost *:80>' & CR;
    apacheConf = apacheConf & getServerAdmin();
    apacheConf = apacheConf & 'DocumentRoot "#websitePath#"' & CR;
    apacheConf = apacheConf & 'ServerName #websiteURL#' & CR;
    apacheConf = apacheConf & getEngineConnection( engine, websitePath );
    apacheConf = apacheConf & '</VirtualHost>' & CR;

    print.line().blueLine( apacheConf );

    return apacheConf;
   
}

Wait, that's not the same code, no, I took the LOGIC out of this function too. Appending to the ApacheConf variable is easy, but if there was a decision to be made, or more complex output, I refactored that out too.


function getServerAdmin(){
    if ( this.settingsObj.get("ApacheAdminEmail") GT ""){
        return 'ServerAdmin ' & this.settingsObj.get("ApacheAdminEmail") & CR;
    }
    else {
        return '';
    }
}

This getServerAdmin function hides the task of determining if we need a ServerAdmin line in our config, and how to output it from our settings.

 

function getEngineConnection( engine, websitePath ){
    var returnConf = '';
   
    if ( engine == 'CF11'){
        returnConf = returnConf & 'Include "' & this.settingsObj.get("cf11ConnFile") & '"' & CR;
    }
    else {
        returnConf = returnConf & '<Proxy *>' & CR;
        returnConf = returnConf & 'Allow from 127.0.0.1' & CR;
        returnConf = returnConf & '</Proxy>' & CR;
        returnConf = returnConf & 'ProxyPreserveHost On' & CR;
        returnConf = returnConf & 'ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:';
        returnConf = returnConf & getEnginePort( engine );
        returnConf = returnConf & getProxyPath( websitePath ) & '/$1$2' & CR;

    }
    return returnConf;
}

getEngineConnection has a lot of complexity, and that too actually has some refactored functions too. The sub functions hide the complexity from the parent functions… so you can quickly and easily determine what the code is doing. The function names are self commenting, and if there is anything that doesn’t make sense, or is doing too much, I pull it out into its own function. getEnginePort and getProxyPath are both helper functions, but they make the getEngineConnection function much easier to read.

function getEnginePort( engine ){
    if ( engine == "Railo" ){
        return this.settingsObj.get("railoAJPPort");
    }
    else {
        return this.settingsObj.get("luceeAJPPort");
    }
}

function getProxyPath( required string websitePath ) {
    return replacenocase( websitePath, this.settingsObj.get("tomcatRootPath"), "" );
}



Last piece of Code

BEFORE:

var apacheConfPath = this.settingsObj.get("apacheConfPath");
print.line( 'apacheConfPath = ' & apacheConfPath );
if( apacheConfPath == '' ) {
    return error( "#apacheConfPath#: No such directory" );
}
   
apacheConfPath = fileSystemUtil.resolvePath( apacheConfPath );
print.line( 'apacheConfPath = ' & apacheConfPath );
if( !directoryExists( apacheConfPath ) ) {
    return error( "#apacheConfPath#: No such directory" );
}
print.line( 'Writing Conf to File: ' & apacheConfPath & '/' & websiteURL & '.conf' );
fileWrite( apacheConfPath & '/' & websiteURL & '.conf', apacheConf );

Which outputs the Conf to a File so we’ll make a function called outputConfToFile. 

NEW FUNCTION:

function outputConfToFile( required string apacheConf, required string websiteURL ){
    var apacheConfPath = this.settingsObj.get("apacheConfPath");
    print.line( 'apacheConfPath = ' & apacheConfPath );
    if( apacheConfPath == '' ) {
        return error( "#apacheConfPath#: No such directory" );
    }
   
    apacheConfPath = fileSystemUtil.resolvePath( apacheConfPath );
    print.line( 'apacheConfPath = ' & apacheConfPath );
    if( !directoryExists( apacheConfPath ) ) {
        return error( "#apacheConfPath#: No such directory" );
    }
    print.line( 'Writing Conf to File: ' & apacheConfPath & '/' & websiteURL & '.conf' );
    fileWrite( apacheConfPath & '/' & websiteURL & '.conf', apacheConf );
}

Looking at this function, I already want to break it up into multiple functions, but for now I will leave it as one function… as we have done a lot of work for one post.
Lets look at our main run function now.

function run( required string websitePath, required string websiteURL, required string engine ){
        this.settingsObj = createObject( "component", "set.settings");
       
        ARGUMENTS.websitePath = validateWebsitePath( ARGUMENTS.websitePath );
        validateRequiredSettings( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );
        var apacheConf = generateApacheConf( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );
       outputConfToFile( apacheConf, websiteURL );
       runCommand( "kiwiSays restartApache" );
}


Sharp, clean, simple, and to the point.
If you need to know what it does, you can see that from the code, if you want to learn more, you can peel back a layer.
Writing this, I can already see what could or should be broken down more… which is a good sign I’m on the right path.
Just because you are writing a command, doesn’t mean it cannot be clean code.

Full File is here for reference:

component extends="commandbox.system.BaseCommand" {
   
    property name="settingsObj";
        
    /**
    * @websitePath.hint Path to the Website Directory - .\ for current directory  
    * @websiteURL.hint The Website URL
    * @engine.hint The CFML Engine you want the Website to run on - lucee, railo or cf11
    * @engine.options Lucee,Railo,CF11
    */
    function run( required string websitePath, required string websiteURL, required string engine ){
        this.settingsObj = createObject( "component", "set.settings");
       
        ARGUMENTS.websitePath = validateWebsitePath( ARGUMENTS.websitePath );
        validateRequiredSettings( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );
        var apacheConf = generateApacheConf( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );
        outputConfToFile( apacheConf, websiteURL );
        runCommand( "kiwiSays restartApache" );
    }
   
    function validateWebsitePath( websitePath ){
        websitePath = fileSystemUtil.resolvePath( ARGUMENTS.websitePath );
        if( !directoryExists( ARGUMENTS.websitePath ) ) {
             error( "#ARGUMENTS.websitePath#: No such directory - Please enter a valid relative or full path" );
        }
        return websitePath;  
    }
   
    function validateRequiredSettings( required string websiteURL, required string websitePath, required string engine ){
        if ( validateEngine( ARGUMENTS.engine ) != ""){
            return error( validateEngine( ARGUMENTS.engine ) );
        }
        return "";
    }
   
    function validateEngine( engine ){
        if ( listFind( "lucee,railo,cf11", ARGUMENTS.engine) == false ){
            return 'Please select a valid Engine - Lucee, Railo or CF11';
        }
    }
   
    function generateApacheConf( required string websiteURL, required string websitePath, required string engine ){
        var apacheConf = "";
   
        apacheConf = apacheConf & '<VirtualHost *:80>' & CR;
        apacheConf = apacheConf & getServerAdmin();
        apacheConf = apacheConf & 'DocumentRoot "#websitePath#"' & CR;
        apacheConf = apacheConf & 'ServerName #websiteURL#' & CR;
        apacheConf = apacheConf & getEngineConnection( engine, websitePath );
        apacheConf = apacheConf & '</VirtualHost>' & CR;

        print.line().blueLine( apacheConf );
        return apacheConf;
    }
   
    function outputConfToFile( required string apacheConf, required string websiteURL ){
       var apacheConfPath = this.settingsObj.get("apacheConfPath");
       print.line( 'apacheConfPath = ' & apacheConfPath );
       if( apacheConfPath == '' ) {
           return error( "#apacheConfPath#: No such directory" );
       }
       apacheConfPath = fileSystemUtil.resolvePath( apacheConfPath );
        print.line( 'apacheConfPath = ' & apacheConfPath );
        if( !directoryExists( apacheConfPath ) ) {
             return error( "#apacheConfPath#: No such directory" );
        }
        print.line( 'Writing Conf to File: ' & apacheConfPath & '/' & websiteURL & '.conf' );
        fileWrite( apacheConfPath & '/' & websiteURL & '.conf', apacheConf );
    }
   
    function getEngineConnection( engine, websitePath ){
       var returnConf = '';
        if ( engine == 'CF11'){
            returnConf = returnConf & 'Include "' & this.settingsObj.get("cf11ConnFile") & '"' & CR;
        }
        else {
            returnConf = returnConf & '<Proxy *>' & CR;
            returnConf = returnConf & 'Allow from 127.0.0.1' & CR;
            returnConf = returnConf & '</Proxy>' & CR;
            returnConf = returnConf & 'ProxyPreserveHost On' & CR;
            returnConf = returnConf & 'ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:';
            returnConf = returnConf & getEnginePort( engine );
            returnConf = returnConf & getProxyPath( websitePath ) & '/$1$2' & CR;
        }
        return returnConf;
    }
   
    function getEnginePort( engine ){
        if ( engine == "Railo" ){
            return this.settingsObj.get("railoAJPPort");
        }
        else {
            return this.settingsObj.get("luceeAJPPort");
        }
    }
   
    function getServerAdmin(){
        if ( this.settingsObj.get("ApacheAdminEmail") GT ""){
            return 'ServerAdmin ' & this.settingsObj.get("ApacheAdminEmail") & CR;
        }
        else {
            return '';
        }
    }
   
    function getProxyPath( required string websitePath ) {
        return replacenocase( websitePath, this.settingsObj.get("tomcatRootPath"), "" );
    }
}

 

Add Your Comment

Recent Entries

Into the Box 2024 - Conference Slides are out!

Into the Box 2024 - Conference Slides are out!

Did you miss the live event? Get a glimpse of the fantastic content our speakers covered by reviewing their slides. Dive into topics like AWS, Cloud, Hosting, Migration, Security, CFML, Java, CommandBox, ContentBox, and more. Don't miss these valuable resources, and join us for upcoming events!

Maria Jose Herrera
Maria Jose Herrera
July 03, 2024
Ortus June 2024 Newsletter!

Ortus June 2024 Newsletter!

Welcome to the latest edition of the Ortus Newsletter! This month, we're excited to bring you highlights from our sessions at CFCamp and Open South Code, as well as a sneak peek into our upcoming events. Discover the latest developments in BoxLang, our dynamic new JVM language, and catch up on all the insightful presentations by our expert team. Let's dive in!

Maria Jose Herrera
Maria Jose Herrera
June 28, 2024
BoxLang June 2024 Newsletter!

BoxLang June 2024 Newsletter!

We're thrilled to bring you the latest updates and exciting developments from the world of BoxLang. This month, we're diving into the newest beta release, introducing a new podcast series, showcasing innovative integrations, and sharing insights from recent events. Whether you're a seasoned developer or just getting started, there's something here for everyone to explore and enjoy.

Maria Jose Herrera
Maria Jose Herrera
June 28, 2024