Sunday, March 2, 2014

SIP UUI Header Parsing in Java

Summary

This article is going to expand on the SIP UUI decoding topic I introduced in a previous post.

I'm going to discuss the development of a recursive descent parser for the SIP UUI header per the definition in the current IETF draft.  I wrote the parser in Java and implemented it as SOAP-based web service.  The motivation for this exercise is to build an end-to-end SIP UUI integration with Genesys call routing.  I'll discuss that in a later post.

Application Layout


Figure 1



Parser Implementation

That IETF draft includes a Backus-Naur Form definition of the SIP UUI header.  This definition corresponds to a context-free grammar (CFG).  A CFG consists of production rules, terminals, non-terminals, and one special non-terminal known as the 'start' symbol.  Terminals equate to base symbols such as keywords or literals.  Production rules define how non-terminals expand to terminals.  The left side of a production rule is a non-terminal.  The right side is a sequence of terminals and non-terminals. Example, using the IETF spec:

UUI -> "User-to-User" HCOLON uui-value *(COMMA uui-value)

This is a production rule.  UUI is a non-terminal and also the start symbol.  uui-value is another non-terminal in this production.  The terminals of this production are:  the "User-to-User" keyword, HCOLON (aka ':'), and COMMA (aka ',').  *(COMMA uui-value) is a regular expression that means zero or more occurrences of a comma (',') and uui-value can occur after the first uui-value.

Developing a parser for simple CFG's such as this is straightforward.  In fact, there are many tools out there to auto-generate a parser.  For this simple grammar, I wrote the parser by hand.  Fortunately, this is a quick process in Java.

I leveraged the Java built-in StringTokenizer class to create a simple lexical analyzer.  The analyzer ignores white space and breaks a SIP UUI header input up into a series of tokens.  Those tokens are then 'match'ed per the grammar's production rules.  Code snippet below:

tokenizer = new StringTokenizer(header.replaceAll("\\s", ""), ":;,\"=", true);
lookahead = tokenizer.nextToken();


....



private void match(String token) throws Exception
{
  logger.debug("Entering match(token=" + token  + ")");
  if (lookahead.equalsIgnoreCase(token))
  {
  if (tokenizer.hasMoreTokens())
  lookahead = tokenizer.nextToken();
  else
  lookahead = "\0";
  logger.debug("Exiting match()");
  }
  else
  {

    String errMsg = "Syntax Error - Expected:" + token + ", Found:" + lookahead;
  logger.error("Error in parse():" + errMsg);
  throw new Exception(errMsg);
  }

}

Each non-terminal of the grammar corresponds a Java method.  That method 'match'es terminals and/or calls further methods for non-terminals, per the production rules.  In some cases, a recursive call is made for the non-terminal hence the name 'recursive' descent parser.  Example methods below for UUI and uui-value non-terminals.  Genesys strips the 'User-to-User:' preamble, so I'm not 'match'ing it below (commented out).

private void uui() throws Exception
{
  logger.debug("Entering uui()");


  /*
  The next 2 tokens get stripped during the Genesys SIP to TLib conversion hence they're commented out so as to be ignored.
  */
  //match(USERTOUSER);
  //match(COLON);


  uuiValue();
  while (lookahead.equals(COMMA))
  {
  match(COMMA);
  uuiValue();
  }
  logger.debug("Exiting uui()");
}

With the UUI header parser in place, decoding and encoding a header to/from hex and ASCII is a simple matter.

public static String[] decode(String header)
{
  logger.debug("Entering decode(header=" + header  + ")");

  UUIParser parser = new UUIParser(header);
  String[] strValues = null;
  try
  {
  ArrayList<String> hexValues = parser.parse();
  if (hexValues.size() > 0)
  {
  strValues = new String[hexValues.size()];
  for (int i = 0; i < hexValues.size(); i++)
strValues[i] = Decoder.hexToString(hexValues.get(i));
  }
  }
  catch (Exception e)
  {
  String errMsg = "error: " + e.toString();
  logger.error("Error in decode():" + errMsg);
  }
  logger.debug("Exiting decode()");
  return strValues;
}

Next step was turning this into a web service.  Auto-generating a web service from Java code is super easy in Eclipse.  A very nice tutorial on how to do that is here.  Using the Eclipse tools, I generated the web service and client stub (for testing).

Execution

Below are sample encode and decode SOAP calls from curl and their output:

UUI encoding the string 'test'.

SOAP input envelope - soapencode.xml:
-------------------------------------------------------
<?xml version='1.0' encoding='UTF-8'?><soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"><soapenv:Body><ns1:encode xmlns:ns1="http://sipuui"><ns1
:values>test</ns1:values></ns1:encode></soapenv:Body></soapenv:Envelope>

------------------------------------------------------

curl -v -H 'Content-Type: application/soap+xml; charset=UTF-8; action="urn:encode"' -X POST -d "@soapencode.xml" http://localhost:8080/sipuui/services/UUITranscoder.UUITranscoderHttpSoap11Endpoint/
* Adding handle: conn: 0xa54e90
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0xa54e90) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 8080 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /sipuui/services/UUITranscoder.UUITranscoderHttpSoap11Endpoint/ HTTP/1.1
> User-Agent: curl/7.32.0
> Host: localhost:8080
> Accept: */*
> Content-Type: application/soap+xml; charset=UTF-8; action="urn:encode"
> Content-Length: 240
>
* upload completely sent off: 240 out of 240 bytes
< HTTP/1.1 200 OK
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Content-Type: application/soap+xml; action="urn:encodeResponse";charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sun, 02 Mar 2014 16:44:07 GMT
<
* Connection #0 to host localhost left intact
<?xml version='1.0' encoding='UTF-8'?><soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"><soapenv:Body><ns:encodeResponse xmlns:ns="http://sipuui"><ns:return>74657374;encoding=hex</ns:return></ns:encodeResponse></soapenv:Body></soapenv:Envelope>





Now, parsing and decoding that same header string.

SOAP input envelope - soapdecode.xml:
-------------------------------------------------------
<?xml version='1.0' encoding='UTF-8'?><soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"><soapenv:Body><ns1:decode xmlns:ns1="http://sipuui"><ns1
:header>74657374;encoding=hex</ns1:header></ns1:decode></soapenv:Body></soapenv:Envelope>
-------------------------------------------------------


curl -v -H 'Content-Type: application/soap+xml; charset=UTF-8; action="urn:decode"' -X POST -d "@soapdecode.xml" http://localhost:8080/sipuui/services/UUITranscoder.UUITranscoderHttpSoap11Endpoint/
* Adding handle: conn: 0xf42ea0
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0xf42ea0) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 8080 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /sipuui/services/UUITranscoder.UUITranscoderHttpSoap11Endpoint/ HTTP/1.1
> User-Agent: curl/7.32.0
> Host: localhost:8080
> Accept: */*
> Content-Type: application/soap+xml; charset=UTF-8; action="urn:decode"
> Content-Length: 257
>
* upload completely sent off: 257 out of 257 bytes
< HTTP/1.1 200 OK
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Content-Type: application/soap+xml; action="urn:decodeResponse";charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sun, 02 Mar 2014 17:25:16 GMT
<
* Connection #0 to host localhost left intact
<?xml version='1.0' encoding='UTF-8'?><soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"><soapenv:Body><ns:decodeResponse xmlns:ns="http://sipuui"><ns:return>test</ns:return></ns:decodeResponse></soapenv:Body></soapenv:Envelope>




No comments:

Post a Comment