E4SS Cookbook Print
A cookbook of common ECharts for SIP Servlets programming tasks.

Discuss this article on the forums. (0 posts)

The E4SS Cookbook

Table of contents

Creating a back-to-back user agent

Use the B2buaFSM fragment from the public SIP/ECharts release, along with a RequestModifier that specifies how the incoming INVITE should be modified before sending it out. (This can be null if no modifications are desired.)

state MY_B2BUA : B2buaSafeFSM(box, caller, callee, myRequestModifier);

transition MY_B2BUA.TERMINAL --> END;

// terminal state
state END;

MY_B2BUA instantiates a B2buaSafeFSM with the specified ports and RequestModifier. In order to properly terminate the machine, the programmer must specify a transition to a terminal state: a simple (non-nested) state with no outgoing transitions.

In the code snippet above, caller and callee are SipPorts, defined at the machine level, and typically created in the machine constructor:

caller = box.createSipPort("caller");
callee = box.createSipPort("callee");

myRequestModifier could also be initialized in the machine constructor. For simple cases, the box provides a default RequestModifier that will change the Request-URI of the outgoing INVITE via init-params called newUser, newHost and newPort in sip.xml:

box.getDefaultModifier()

Any parameter that is not specified will not be changed.

Acting transparently after a call is set up

Once a call is set up via B2buaSafeFSM, the programmer may wish to transition to a new state, while retaining the default behavior of propagating requests and responses between the ports. For this, use TransparentFSM:

state PLACE_CALL : B2buaSafeFSM(box, caller, callee, null);

transition PLACE_CALL.ACTIVE.INITIAL_INVITE.SUCCESS - /
    startTime = new Date();
-> TRANSPARENT;

state TRANSPARENT : TransparentFSM(caller, callee);

transition TRANSPARENT.TERMINAL - / {
    stopTime = new Date();
    elapsedMsec = stopTime.getTime() - startTime.getTime();
    logSuccess(elapsedMsec);
} -> END;

// handle termination of PLACE_CALL if it does not
// reach CONNECTED state
//
transition PLACE_CALL.TERMINAL - / logFailure() -> END;

state END;

TransparentFSM is used by B2buaSafeFSM for the default CONNECTED behavior, but the application developer may use it directly. Here the machine will set startTime at the time the call becomes connected, which could be used for call logging. (Methods logSuccess and logFailure would need to be defined by the application developer.)

Modifying outgoing requests from a B2BUA

The application developer needs control over how an incoming INVITE is modified before sending it out. A RequestModifier can be used to change different aspects of a SipServletRequest before sending it out. This base class defines three methods:

  • URI getModifiedRequestURI(SipServletRequest req) : specify a new Request URI
  • Address getModifiedFromAddress(SipServletRequest req) : specify a new From header
  • void modify(SipServletRequest req): modify the request in other ways (e.g., add a header)

To use in conjunction with a B2buaSafeFSM fragment, define a subclass that implements the desired behavior:

public MyFSM(...) {
    ...
    RequestModifier myReqModifier = new CustomRequestModifier(...);
}

state FORWARD : B2buaSafeFSM(box, caller, callee, myReqModifer);

One useful subclass is already defined: RequestURIRequestModifier changes only the Request-URI of the outgoing INVITE.

state FORWARD : B2buaSafeFSM(box, caller, callee, new RequestURIRequestModifier(newURI));

Getting access to messages that fire transitions

It is commonplace to need access to the particular message that satisfies a transition rule, in order to process it or save the reference for later use. To access the message object, use the reserved variable message:

transition AWAIT_FINAL_RESPONSE - p ? SuccessResponse / {
    saveSDP(message.getContent());
    p ! message.createAck();
} -> CONNECTED;

The class of message will be the type specified in the transition rule. In this example, message will be declared as a SuccessResponse.

Creating a B2BUA conditioned on an initial request

Invoking the B2buaSafeFSM fragment to handle all of call setup handles many cases. However sometimes the application should behave differently depending on some characteristics of the initial request. In this case, the application should handle the initial request itself and invoke B2buaFSM at a later point in the call flow, if appropriate.

Consider a 900-number blocking service. If the initial request is destined for a 900 number, the application should reject the call, otherwise it should continue the call:

public machine Block900FSM {

    ...
    BoxPort boxPort;

    boolean is900(SipServletRequest req) {
        ...
    }

    public Block900FSM {
        ...
        boxPort = box.getBoxPort();
    }

    initial state RECEIVE_INVITE;

    transition RECEIVE_INVITE - boxPort ? Invite [ is900(message) ] / {
            caller.bind(message);
            caller ! message.createResponse(SipServletResponse.SC_FORBIDDEN);
        } -> END
    else / caller.bind(message);
        -> B2BUA.SEND_INVITE;

    state B2BUA : B2buaSafeFSM(box, caller, callee, reqModifier);

    transition B2BUA.TERMINAL --> END;

    state END;

There are several points to note in this example:

  • Initial requests arrive on a special port called a BoxPort. This gives the application developer the freedom to associate (bind) a request to the desired SipPort at runtime. This can be useful when an application can be invoked on behalf of the caller or the called party, and that information is not known until runtime. If the B2buaSafeFSM fragment is used to handle the initial request, it handles the message on the BoxPort and binds the (SipSession of the) message to the SipPort.
  • In the case where is900 returns true, the call will be rejected right away by the application. Since there is no fragment to perform the required bind to the port, then the application must do so. In order to send messages on a SipPort, it must be bound to a SipSession. However, this step is often handled by fragments, freeing the developer from the responsibility.
  • In the case where is900 returns false, control is dispatched to B2buaFSM to handle the rest of call setup. However it is necessary to supply the desired initial state (SEND_INVITE), since the default initial state of that fragment waits for an initial request, which in this case has already been received and processed.

Overriding default behavior

To override default behavior in a sub-machine, provide a parent-level transition that has precedence based on the ECharts transition priority rules.

state CONNECTED : TransparentFSM(caller, callee, myRequestModifier);

// respond to INFO messages with error
transition CONNECTED - callee ? Info /
    callee ! message.createResponse(SipServletResponse.SC_METHOD_NOT_ALLOWED)
-> CONNECTED.DEEP_HISTORY;

In this case, the transition defined in this machine refers to a more specific message class than the sub-machine does, as TransparentFSM is looking for any Request. This causes the parent machine to handle the transition and override the sub-machine's default behavior.

If the message class is the same, the next priority rule that is invoked is most specific source state. To get the parent to override the sub-machine, it must specify the complete state of the sub-machine as the source state of its transition:

transition CONNECTED.TRANSPARENT - callee ? Request /
    callee ! message.createResponse(SipServletResponse.SC_METHOD_NOT_ALLOWED)
-> CONNECTED.DEEP_HISTORY;

Note that the target of both transitions is the DEEP_HISTORY pseudo-state. This causes the submachine to keep its state and instance variables intact; otherwise the machine would be re-initialized.

Executing transitions based on timers

In addition to firing a transition based on received messages, the application developer can specify a transition that will fire after a specified delay:

transition RINGING - delay(ringNoAnswerTimeoutMsec) -> NO_ANSWER;

transition RINGING - callee ? SuccessResponse /
    callee ! message.createAck()
-> CONNECTED;

The timer begins as soon as the machine enters the source state (RINGING in this example). If the specified timeout is 0, the transition will fire immediately. If the timeout is negative, it will never fire. If the machine moves away from this source state through other means (like the transition to CONNECTED above), the timer is cancelled.

The SIP/ECharts framework uses the ServletTimer mechanism to implement these transitions, but the application developer does not need to be aware of that.

Using the response class hierarchy

Since transition priority rules depend on specificity of message class, this can be used to concisely specify different behavior based on the response class hierarchy:

transition AWAIT_FINAL_RESPONSE - p ? SuccessResponse /
    p ! message.createAck();
-> CONNECTED;

transition AWAIT_FINAL_RESPONSE - p ? FinalResponse -> END;

In this example, if a SuccessResponse is received, an ACK will be sent and the machine will move to the CONNECTED state. However if any other FinalResponse is received, no ACK is required, and the machine will move to END. For the full class hierarchy, see the javadoc.

Getting access to the SipFactory

Sometimes applications need access to the SipFactory to perform some utility functions. This can be obtained by calling a method on the FeatureBox:

<* SipURI sipURI = box.getFactory().createSipURI("user", "example.com"); *>

Note that the code above is contained in host-language escape delimiters. This is required by ECharts because the snippet declares a new variable.

Understanding the complete transition declaration

There are only two required elements of a valid transition declaration: a source state and a target state. The capability of ECharts to embed machines within states makes such transitions common in parent machines, as the events which lead to the transitions are hidden in the sub-machines:

transition PLACE_CALL.CONNECTED --> TRANSPARENT;

However there are several more optional elements in a transition declaration, as show below:

transition SOURCE_STATE - portRef ? ArbitraryObject [ arbitaryBooleanExpr ] / {
   arbitraryActions();
   otherPortRef ! ObjectToSend;
   // ...
} -> TARGET_STATE;

This transition will fire when:

  • the Machine is in SOURCE_STATE, AND
  • a message of class ArbitaryObject is received on the port referred to by portRef, AND
  • arbitraryBooleanExpr evaluates to true.

In the course of firing, the list of arbitary actions will be performed, including sending messages on ports if desired. Of course, if the sending port is a SipPort, then only properly constructed SIP messages can be sent on the port.

After performing the specified actions, the machine enters TARGET_STATE.

Note that the simple transition above is just a special case of the general transition declaration, with all optional components omitted.

Defining if/else transitions

Often it is convenient to move to different target states based on some runtime computation other than just the message class. ECharts provides an if/else syntax to compactly specify the desired behavior:

transition AWAIT_FINAL_RESPONSE - p ? ProvisionalResponse [ hasSDP(message) ] /
        earlyMediaResponse = message;
    -> EARLY_MEDIA
else
    -> AWAIT_FINAL_RESPONSE;

In this example, if a ProvisionalResponse is received on p, then the user-defined boolean method hasSDP will be called. If it evaluates to true, then the first action/target is chosen. If it evaluates to false, then the second target is chosen.

Multiple guard conditions can be tested before the else is specified, and each target transition can have its own set of actions.

Note that access to the Object that matches the transition rule is provided by use of the reserved variable message.

Understanding termination behavior

SIP/ECharts provides the application developer with a number of mechanisms to aid in properly terminating calls. Some fragments contain code to perform normal end-to-end teardown, which will often be used. In addition, there is a straightforward way to instruct the SIP/ECharts framework to tear down all legs of a call. Finally, if the application does not explicitly handle BYE or CANCEL in any given state, the SIP/ECharts framework will properly terminate all call legs. Here each of these aspects is explained in more detail.

Using fragments for end-to-end teardown

In a normal two-party call (B2BUA), it is good practice to tear down calls end-to-end. This means that when a BYE is received, it is propagated to the other port with all non-system headers and the optional body copied. The application will wait for the 200 response to the BYE and propagate the response (again copying as necessary) to the port that sent the BYE. This allows any semantically important information to be transparently propagated in these messages. For instance, the BYE might contain a header indicating that the call was torn down because a time limit was exceeded, which may be of interest to another application.

TransparentFSM provides this end-to-end teardown behavior. Since B2buaFSM uses TransparentFSM in its CONNECTED state, it also provides this behavior. Using these fragments wherever possible will automatically get the desired behavior. However the parent machine must ensure it transitions to a terminal state when the fragment terminates.

state TRANSPARENT : TransparentFSM(caller, callee);

// Refer to TERMINAL pseudo-state: will be matched by
// any state that meets the definition of terminal.
// This is simply a shorthand for supplying a transition
// for each such state.
//
transition TRANSPARENT.TERMINAL --> END;

// simple state with no outgoing transitions, so it is terminal
//
state END;

Programatically ending a call

Sometimes an application may need to terminate a call on its own volition, rather than propagating a received message that initiates the termination behavior. This is very easy to do: simply transition to a terminal state.

// a non-SIP entity is instructing the application to
// tear down all calls
transition LOTS_OF_THINGS_HAPPENING - nonSip ? TerminateAllCalls
-> END;

// simple state, no outgoing transitions
state END;

The SIP/ECharts framework will initiate teardown behavior on all ports in the box. Once the ports have been properly terminated, the sessions are invalidated and the machine terminates execution.

Letting the framework help you out

Using TransparentFSM will go a long way toward handling termination conditions properly. However there are plenty of cases where transparent handling is not appropriate, or even well-defined. For instance, when switching a caller from one endpoint to another, what should happen if the caller sends BYE?

The SIP/ECharts framework provides default handling for any unhandled BYE or CANCEL message received. In this case, the framework initiates teardown of all ports in the box, just as in the programmer-initiated case above. In addition, the same treatment applies if an Exception is encountered in the execution of the machine logic.

TransparentFSM handles BYE and CANCEL messages, so the framework does not intervene when the machine is in such a state. However for other states, the framework frees the developer from having to explicitly code transitions for these messages in any state where they might be received.

If non-default behavior is desired, then the application must handle the messages explicitly. For example, consider a sequence dialing application where the called party hangs up after talking for a while. In this case, the application should redirect the subscriber to a media server to enter the next number to dial, rather than tear down all calls.

transition CONNECTED - farParty ? Bye /
    farParty ! message.createResponse(200)
-> REDIRECT_SUB_TO_MEDIA_SERVER;

Application developers can also make use of the port termination machine in their own machines, so that they do not need to worry about the proper way to tear down a particular port:

state SWITCH : concurrent {
    state TEARDOWN_OLD_CALL : SipPortTeardownFSM(oldcall);
    state PLACE_NEW_CALL : ...
};

Deferring message processing

It is sometimes convenient to temporarily defer message processing on a particular port while dealing with a different port. This can be achieved simply by not referring to the port when in that state:

state AWAIT_CALLEE_FINAL_RESPONSE;

transition AWAIT_CALLEE_FINAL_RESPONSE - callee ? FinalResponse
-> CONNECTED;

state CONNECTED;

transition CONNECTED - caller ? Invite /
   reinvite = message;
-> HANDLE_REINVITE;

Note that no transitions are defined on the caller port in the AWAIT_CALLEE_FINAL_RESPONSE state. If any messages are received on this port while the machine is in this state, they will be queued until a state is reached where caller messages are processed. In this example, that is the CONNECTED state.

Note that this mechanism should be used with care, as SIP entities expect to receive responses and ACKs in a timely manner. However in certain cases it can be convenient to temporarily defer message processing in order to avoid state-space expansion.

Ensuring messages are not deferred

Since ECharts supports message deferral as described above, the developer should be mindful to avoid such deferral if it is not desired. To avoid message deferral, define a transition on the desired port. However when this is done, the programmer must define behavior for all possible messages on that port. This can often be done via a default transition on a broad message class, with subclass overrides are desired.

One particular case to keep in mind is that auto-termination semantics requires that the port be active. In order to have your application properly terminated and all ports torn down when an unhandled BYE or CANCEL is received, make sure the port is active. One way to do this is:

state SWITCH_CALLEE : SomeSwitchFragmentFSM(callee, ...);

// absorb (almost) anything on caller port
//
transition SWITCH_CALLEE - caller ? Object
-> SWITCH_CALLEE.DEEP_HISTORY;

The SWITCH_CALLEE state may be very busy processing messages on the callee port, but the Object transition on caller ensures that this port is also active. Then if a BYE or CANCEL is received on that port, the termination handler will take over. (Note that this is because the termination handler refers to messages that are more specific than Object, and thus have priority.)

Getting access to sip.xml properties

It can be convenient to parameterize feature data (addresses, etc.) based on parameters defined in the sip.xml deployment descriptor. FeatureBox instances are provided a reference to a java.util.Properties object as a means to access any init-param defined in the application's deployment descriptor.

vxmlServer = servletProps.getProperty("vxmlServer");  

Use the SIP Servlet Application Router mechanism

JSR 289 specifies a standard mechanism for invoking multiple applications in a deterministic order for a single call. (See the JSR 289 site (http://jcp.org/en/jsr/detail?id=289) for details of this mechanism, known as the application router.)

ECharts for SIP Servlets provides API support for using a similar mechanism within a JSR116 container, if desired. The support comes in the form of methods to create INVITE messages on a SipPort. The javadoc provides full details, but the support is outlined below:

/** Creates an INVITE message based on the supplied message,
 *  applies the supplied request modifier and the supplied
 *  routing directive.  This directive can be NEW or CONTINUE,
 *  as defined in JSR 289.
 */
SipPort.createInvite(
    Invite invite,
    RequestModifier reqModifier,
    SipApplicationRoutingDirective routingDirective)

/** Creates an INVITE message with the supplied From and To.
 *  This will be presented to the application router (if
 *  present) as a NEW request, as defined in JSR 289.
 */
SipPort.createInvite(
    Address from,
    Address to)

SipPort.createInvite(
    URI from,
    URI to)

/** Creates an INVITE message based on the supplied message
 *  and applies the supplied request modifier.  This request
 *  will be presented to the application router (if present)
 *  as a CONTINUE, as defined in JSR 289.
 */
SipPort.createInvite(
    Invite invite,
    RequestModifier reqModifier)

/** Creates an INVITE message based on the supplied message.
 *  This request will be presented to the application router
 *  (if present) as a CONTINUE, as defined in JSR 289.
 */
SipPort.createInvite(
    Invite invite,
    RequestModifier reqModifier)

/** Returns the subscriber URI associated with an initial 
 *  request that a servlet receives.
 */
SipApplicationRouterWrapper.getSubscriberURI(
    SipServletRequest req)

/** Returns the routing region associated with an initial 
 *  request that a servlet receives.
 */
SipApplicationRouterWrapper.getRoutingRegion(
    SipServletRequest req)

Create a feature that works for callee and caller

Some features only apply to calling parties (e.g., sub-account billing) or to called parties (e.g., redirect to voicemail). However some features can potentially apply to either the caller or callee. For these features, it is desirable to write the feature logic one time, and have the feature determine at run-time if it is being invoked on behalf of the caller or callee.

The SIP Servlet Application Router can be employed to specify which addresses subscribe to a particular application, and in which zone(s). If you want an application to be invoked on behalf of a caller, then a subscription should be created in the originating region. For a callee application, a terminating region subscription is required. Details of the subscription mechanism are available at TODO.

Once this mechanism is in place, then a SIP/ECharts program can determine the zone in which the application instance is invoked, as well as the subscribing address. The application logic may be easier to describe by using ports associated with the subscriber and the farparty rather than the caller and callee. However the SIP call model is substantially different for the caller and callee. In this case, it may be convenient to set up port aliases that are assigned at runtime. The distinction between caller and callee can be used when convenient, but for application logic that focuses on the port associated with the subscriber, then the aliases can be used.

Consider this (partial) application which sets up a call in either region. When the far party hangs up, the subscriber will be connected to a resource to hear the subscriber's remaining balance. The initial logic concentrates on caller and callee to establish the SIP dialog. However the logic to detect the far party hangup and place a new call uses the subscriber and farparty aliases.

<*
SipPort subscriber, farparty;
SipPort caller, callee;
Invite savedInvite;
SipApplicationRoutingRegion region;
RequestModifier reqModifier;
*>
... // create caller and callee SipPorts in constructor
...
initial state INIT;

state CHECK_REGION;

state SEND_INVITE;

state MONITOR_CALLEE;

transition INIT - boxPort?Invite / {
    savedInvite = message;
    region = SipApplicationRouterWrapper.getRoutingRegion(savedInvite);
} -> CHECK_REGION;

transition CHECK_REGION 
- [ region.getType() == SipApplicationRoutingRegionType.ORIGINATING ] / {
        subscriber = caller;
        farparty = callee;
    } -> SEND_INVITE
else [ region.getType() == SipApplicationRoutingRegionType.TERMINATING ] / {
        farparty = caller;
        subscriber = callee;
    } -> SEND_INVITE;

transition SEND_INVITE - / {
    // set up bidirectional association between SipPort and SipSession
    caller.bind(savedInvite);
    calleeInvite = callee.createInvite(savedInvite, reqModifier);
    callee!calleeInvite;
} -> MONITOR_CALLEE;

...

state CONNECTED;

state PLAY_BALANCE_TO_SUBSCRIBER;

/** If the far party hangs up, connect the subscriber to a resource
 * to hear the remaining balance.  Note that the far party might have
 * been the original caller or callee, but this handles either case.
 */
transition CONNECTED - farparty ? Bye /
    farparty ! farparty.createResponse(200, message)
-> PLAY_BALANCE_TO_SUBSCRIBER;

...

Last Updated ( Wednesday, 26 January 2011 )
 
< Prev   Next >
Copyright © 2006-2009 echarts.org