29 August 2013

Project: Making a SIP GUI test harness with Grails and SIPP - Part II

In Part I, we set up SIPP, SIPP Scenarios and created a Grails application with a controller and a domain class.

What we'll start with in Part II is the creation of a service.

But first... what is a service?

A service is a piece of functionality that coordinates logic with the Domain layer. 

We'll use a service to run the all the logic that's needed for the application.  By separating the code out into a service, we move the logic into a layer that isn't the same as the controller.  If we didn't do this, the controller would be handling all the functionality.

Grails looks to split the core functionality and logic into a separate layer (i.e. Service layer) and leave the controller as handing the routing.  In this model of development the controller is left to return values, render values or redirect to a specific view or other controller.

So again, the 3 R's of controllers are:


  • Render
  • Redirect
  • Return
Render is not a common use of a controller.  Returning values and Redirection is the most common activity of a controller.

So now that that's out there, what do we need to do with this service?

We need to put the logic in there, that handles the user input.  Remember our tool offers a UI that a end user can hit, pull up a form and submit a phone number, carrier and proxy.  We want those values to plug into SIPP, and let SIPP make the call and we'll validate if it's successful or not.

Service

To create a new service, in Intellij you can right click the project in the project tree, and then choose 
New | Grails Service
Give it a name... like DialerService

Great, now in the project tree, open the services folder.  You should see your new service listed there.  Go ahead and double click it to open it up.

Let's go ahead and add some logic in here. 
def sipp(phone,ipprox,carrier){
    def dialNum = "sipp -s ${phone} ${ipprox} -sf /carrierTests/outbound/${carrier}_outbound_call.xml -l 1 -m 1 -d 5000".execute().text
    [dialNumResult:dialNum]
    }
}
Let's review what's going on here.  

The service has a method called sipp, which takes three parameters (phone number, the proxy ip and the carrier) - these three parameters are the ones being passed in by the end user on the form.  

The sipp method has a piece of functionality defined as dialNum.  dialNum basically runs our SIPP command line call.  What's in quotes is the string we will execute.  The ${} is a gString... or groovy string.  Groovy strings are there to pull in those parameter values being passed through.  The .text() may not seem logical, but I actually want it there.  It will take the output from SIPP (with the call data) and convert that to text for us.  That way if someone says "ok the call went through, but how do I know what it did?" they can have a record of the actual call process.

Lastly, what's that square bracket stuff?
The square bracket is a return.  We are saying set up this variable dialNumResult to have the value of dialNum... and pass it back.

In Grails, the last line of a method is the last action returned.  In this case, we're returning the value of dialNum.  



Controller

Back in the controller, we want to pass the variables we are getting from the user input (via the form) through to the service we made.  

So under def outboundCallInstance = new OutboundCall(params) add these lines:
def carrier = params.carrier
def phone = params.phone
def ipprox = params.ipProxy



Great, so do you see how it's working? We're taking the user input on the form, which is going to the controller... and the controller is routing it to the service.  The service will do it's logic/functionality and return the result.  

But this isn't quite done yet.  We are grabbing the data from the form, sure enough, but we aren't passing it to the service.

Above the save method in the controller add this:
def dialerService

In Intellij, it will automatically know that this is a reference to the service we made and put a special icon next to it in the margin.  

Why did we reference the service in camel case, when it's really named DialerService?  Well that's just how you do it.  We're referencing the service in a class and this is how we do it.  

Below the lines we added (after the parmas.ipProxy line) add this line:
outboundCallInstance.results = dialerService.sipp(phone, ipprox, carrier).dialNumResult

If you set it up right it should auto complete a lot of that for you.  We're saying, hey we defined this service, now lets talk to it... we want the outbound call instance's results to be the dialer service and pass into it the 3 variables: 
  • phone
  • ipproxy
  • carrier
...and we expect the dialNumResult returned (this is the output from sipp)

So it should look something like this:

def dialerService
    def save() {
        def outboundCallInstance = new OutboundCall(params)
        def carrier = params.carrier
        def phone= params.phone
        def ipprox = params.ipProxy
        outboundCallInstance.results = dialerService.sipp(phone, ipprox, carrier).dialNumRes

        ....
There will be other code in there, just leave it as it was generated.

To recap:
We are:


  1. Collecting user input on a form
  2. passing that to a controller that sends it on to a Service
  3. The service does some logic on it and returns a result
  4. All this is saved in the db

Validation

If you were to run this, it should make a single call via SIPP to your phone number via a proxy of some sort.  You can test this out by running the application again and filling out the form with valid data for your test environment.

But it's not really validating the phone call.  

First I'll show how to do some simple validation of the SIPP call with some logic in the controller... later on I'll show a different way to validate with some logic in the service.

Back in the controller, let's go to that save method again, and under the outboundCallInstance.results = line, let's add this if statement:


if (dialerService.sipp(dnis,ipprox,carrier).dialNumRes =~ //){
            outboundCallInstance.pass = "PASS"
        } else {
            outboundCallInstance.pass = "FAIL"
        }


This is pretty simple and it won't work. Not yet.  We haven't added the REGEX.

The IF statement is saying, IF the dialer service return =~  (that funny symbol means we're going to use Regular expressions here) // (and what's inside the slashes is what we look for.  If the regular expression matches, then we will assign the string value "PASS" to the Domain Class variable "pass" and if it fails the regular expression, we will assign the string "FAIL" to that Domain Class variable.

If you know SIPP output you know it's difficult to regex on.  This is because SIPP outputs it's results like this:
Successful Call   |0|1
Failed Call         |0|0

That's pretty crazy right?  Because if you make that a map, it would look like this:
Successful Call, 0, 1 Failed Call.

But it's not a failed call, it's a successful call!  Silly SIPP output!


Regex Hell

After many hours of Regular Expression Hell, I created this regex that works for SIPP results:
Successful call\s+([0])\s+1

It basically says, Look for the words Successful call, on the same line, find the 0, and the fond the 1.

That was my solution, yours might be better.

so now the controller should look like:


if (dialerService.sipp(dnis,ipprox,carrier).dialNumRes =~ /Successful call\s+([0])\s+1/){
            outboundCallInstance.pass = "PASS"
        } else {
            outboundCallInstance.pass = "FAIL"
        }


If you were to run this now, it will most likely fail.  Either pass when it should fail, or fail when it should pass. The reason, is the Pipes in the SIPP output.  I've tried a lot of solutions, including escaping the pipes in the Regex, but it still doesn't work right.

To fix it, we need to open the service and add this to the return:
[dialNumRes:dialNum.replaceAll("\\|", "")]


So at the end of dialNum, add a
.replaceAll("\\|", "")


This removes the pipes in the output.  So what's returned is returned without Pipes.

That should be it. It should work just like that. On to Part III (Deployment)

No comments:

Post a Comment