package org.openxdm.xcap.client.test.subscription;
import static org.junit.Assert.assertTrue;
import gov.nist.javax.sip.Utils;
import java.io.IOException;
import java.io.StringReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Properties;
import java.util.concurrent.Semaphore;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MalformedObjectNameException;
import javax.management.ReflectionException;
import javax.naming.NamingException;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.DialogTerminatedEvent;
import javax.sip.IOExceptionEvent;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.SipException;
import javax.sip.SipFactory;
import javax.sip.SipListener;
import javax.sip.SipProvider;
import javax.sip.SipStack;
import javax.sip.TimeoutEvent;
import javax.sip.TransactionTerminatedEvent;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.address.SipURI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.CallIdHeader;
import javax.sip.header.ContactHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.HeaderFactory;
import javax.sip.header.MaxForwardsHeader;
import javax.sip.header.SubscriptionStateHeader;
import javax.sip.header.ToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import junit.framework.JUnit4TestAdapter;
import org.apache.commons.httpclient.HttpException;
import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openxdm.xcap.client.Response;
import org.openxdm.xcap.client.test.AbstractXDMJunitTest;
import org.openxdm.xcap.client.test.ServerConfiguration;
import org.openxdm.xcap.common.key.DocumentUriKey;
import org.openxdm.xcap.common.key.UserDocumentUriKey;
import org.openxdm.xcap.common.xcapdiff.DocumentType;
import org.openxdm.xcap.common.xcapdiff.XcapDiff;
/**
* first puts a new doc through xcap then subscribes it through sip, etag from xcap put response and notify must match.
* Then update document through xcap and a notify with old and new etag should arrive.
* Finally delete document and a notify with old etag should arrive.
* Unsubscribe to clean up.
*/
public class SubscribeDocumentTest extends AbstractXDMJunitTest implements SipListener {
private static Logger logger = Logger.getLogger(SubscribeDocumentTest.class);
public static junit.framework.Test suite() {
return new JUnit4TestAdapter(SubscribeDocumentTest.class);
}
protected enum Tests {
test1,
test2,
test3,
test4
}
protected Tests testRunning;
protected String subscriberUsername = "eduardo";
protected String domain = "openxdm.org";
protected String subscriberSipUri = "sip:"+subscriberUsername+"@" + domain;
protected SipProvider sipProvider;
protected AddressFactory addressFactory;
protected MessageFactory messageFactory;
protected HeaderFactory headerFactory;
protected SipStack sipStack;
protected ContactHeader contactHeader;
protected String notifierPort;
protected String transport;
protected ListeningPoint listeningPoint;
protected Dialog subscriberDialog;
protected String subscriberToTag;
protected String newEtag;
protected String previousEtag;
// a sempahore to control processing using async events
protected Semaphore semaphore = new Semaphore(1);
/**
* the key used to manipulate XCAP document
*/
protected DocumentUriKey getDocumentUriKey() {
return new UserDocumentUriKey(appUsage.getAUID(),subscriberSipUri,documentName);
}
/**
* the content used in initial xcap put and initial subscribe
* @return
*/
protected String getContent() {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<resource-lists xmlns=\"urn:ietf:params:xml:ns:resource-lists\">" +
"<list>" +
"<entry uri=\""+getDocumentUriKey().getDocumentSelector()+"\"/>" +
"</list>" +
"</resource-lists>";
}
/**
* puts a new doc through xcap, store etag of response and then subscribes doc through sip
* @throws InvalidArgumentException
* @throws ParseException
* @throws SipException
*/
@Test
public void test() throws HttpException, IOException, JAXBException, InterruptedException, ParseException, InvalidArgumentException, SipException {
// set test state machine
testRunning = Tests.test1;
// send put request and get response
Response putResponse = client.put(getDocumentUriKey(),appUsage.getMimetype(),getContent(),null);
// check put response
assertTrue("Put response must exists",putResponse != null);
assertTrue("Put response code should be 201",putResponse.getCode() == 201);
// set initial etag
newEtag = putResponse.getETag();
// send subscribe
sendInitialSubscribe();
// let's wait for test to succeed or timeout
synchronized (this) {
this.wait(15000);
}
assertTrue("Test timer expired (15 secs)",passed);
}
private void sendInitialSubscribe() throws ParseException, InvalidArgumentException, SipException {
// create >From Header
Address address = addressFactory.createAddress(subscriberSipUri);
FromHeader fromHeader = headerFactory.createFromHeader(
address, Utils.getInstance().generateTag());
// create To Header
ToHeader toHeader = headerFactory.createToHeader(address,
null);
// Create ViaHeaders
ArrayList viaHeaders = new ArrayList();
int port = sipProvider.getListeningPoint(transport).getPort();
ViaHeader viaHeader = headerFactory.createViaHeader(ServerConfiguration.SERVER_HOST,
port, transport, null);
viaHeaders.add(viaHeader);
// Create a new CallId header
CallIdHeader callIdHeader = sipProvider.getNewCallId();
// Create a new Cseq header
CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(1L,
Request.SUBSCRIBE);
// Create a new MaxForwardsHeader
MaxForwardsHeader maxForwards = headerFactory
.createMaxForwardsHeader(70);
// Create the request.
Request request = messageFactory.createRequest(address.getURI(),
Request.SUBSCRIBE, callIdHeader, cSeqHeader, fromHeader,
toHeader, viaHeaders, maxForwards);
// Create contact headers
SipURI contactURI = addressFactory.createSipURI(subscriberUsername, listeningPoint.getIPAddress());
contactURI.setTransportParam(transport);
contactURI.setPort(port);
// add the contact address.
Address contactAddress = addressFactory.createAddress(contactURI);
// create and save contact header
contactHeader = headerFactory.createContactHeader(contactAddress);
request.addHeader(contactHeader);
// add route
request.addHeader(headerFactory.createRouteHeader(addressFactory
.createAddress("<sip:"+ServerConfiguration.SERVER_HOST+":" + notifierPort
+ ";transport=" + transport + ";lr>")));
// Create an event header for the subscription.
request.addHeader(headerFactory.createEventHeader("xcap-diff"));
// add content
request.setContent(getContent(), headerFactory.createContentTypeHeader("application", "resource-lists+xml"));
// create the client transaction.
ClientTransaction clientTransaction = sipProvider.getNewClientTransaction(request);
// save the dialog
this.subscriberDialog = clientTransaction.getDialog();
// send the request out.
clientTransaction.sendRequest();
}
private DocumentType processNotify(RequestEvent requestEvent) throws ParseException, SipException, InvalidArgumentException, JAXBException {
Request notify = requestEvent.getRequest();
javax.sip.message.Response response = messageFactory.createResponse(200, notify);
// SHOULD add a Contact
ContactHeader contact = (ContactHeader) contactHeader.clone();
((SipURI)contact.getAddress().getURI()).setParameter( "id", "sub" );
response.addHeader( contact );
requestEvent.getServerTransaction().sendResponse(response);
SubscriptionStateHeader subscriptionState = (SubscriptionStateHeader) requestEvent.getRequest().getHeader(SubscriptionStateHeader.NAME);
assertTrue("subscription not active", subscriptionState.getState().equalsIgnoreCase(SubscriptionStateHeader.ACTIVE));
// unmarshall content
StringReader stringReader = new StringReader(new String(requestEvent.getRequest().getRawContent()));
XcapDiff xcapDiff = (XcapDiff) jaxbContext.createUnmarshaller().unmarshal(stringReader);
assertTrue("unexpected xcap root in xcap diff",xcapDiff.getXcapRoot().equals("http://"+ServerConfiguration.SERVER_HOST+":"+ServerConfiguration.SERVER_PORT+ServerConfiguration.SERVER_XCAP_ROOT+"/"));
stringReader.close();
assertTrue("not a single document element inside xcap diff document received", xcapDiff.getDocumentOrElementOrAttribute().size() == 1 && xcapDiff.getDocumentOrElementOrAttribute().get(0) instanceof DocumentType);
return (DocumentType) xcapDiff.getDocumentOrElementOrAttribute().get(0);
}
/*
* etag from xcap put response and notify must match.
*/
private void processTest1Notify(RequestEvent requestEvent) throws JAXBException, ParseException, SipException, InvalidArgumentException {
DocumentType documentType = processNotify(requestEvent);
assertTrue("previous etag is set", documentType.getPreviousEtag() == null || documentType.getPreviousEtag().equals(""));
assertTrue("new etag ("+documentType.getNewEtag()+") doesn't match one received in XCAP PUT response ("+newEtag+")", documentType.getNewEtag() != null && documentType.getNewEtag().equals(newEtag));
}
// ---- TEST 2
private void doTest2() throws InterruptedException, HttpException, IOException {
// set test state machine
testRunning = Tests.test2;
String content =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<resource-lists xmlns=\"urn:ietf:params:xml:ns:resource-lists\">" +
"<list/>" +
"</resource-lists>";
// send put request and get response
Response putResponse = client.put(getDocumentUriKey(),appUsage.getMimetype(),content,null);
// check put response
assertTrue("Put response must exists",putResponse != null);
assertTrue("Put response code should be 200",putResponse.getCode() == 200);
// set etags
previousEtag = newEtag;
newEtag = putResponse.getETag();
}
private void processTest2Notify(RequestEvent requestEvent) throws ParseException, SipException, InvalidArgumentException, JAXBException {
DocumentType documentType = processNotify(requestEvent);
assertTrue("previous etag ("+documentType.getPreviousEtag()+") doesn't match one received in first XCAP PUT response ("+previousEtag+")", documentType.getPreviousEtag() != null && documentType.getPreviousEtag().equals(previousEtag));
assertTrue("new etag ("+documentType.getNewEtag()+") doesn't match one received in XCAP PUT response ("+newEtag+")", documentType.getNewEtag() != null && documentType.getNewEtag().equals(newEtag));
}
// ---- TEST 3
private void doTest3() throws InterruptedException, HttpException, IOException {
// set test state machine
testRunning = Tests.test3;
// send put request and get response
Response deleteResponse = client.delete(getDocumentUriKey(),null);
// set previous etag
previousEtag = newEtag;
// check put response
assertTrue("Delete response must exists",deleteResponse != null);
assertTrue("Delete response code should be 200",deleteResponse.getCode() == 200);
}
private void processTest3Notify(RequestEvent requestEvent) throws ParseException, SipException, InvalidArgumentException, JAXBException {
DocumentType documentType = processNotify(requestEvent);
assertTrue("previous etag doesn't match one received in second XCAP PUT response", documentType.getPreviousEtag() != null && documentType.getPreviousEtag().equals(previousEtag));
assertTrue("new etag is not null", documentType.getNewEtag() == null);
}
// ---- TEST 4
private void doTest4() throws ParseException, SipException, InvalidArgumentException {
testRunning = Tests.test4;
unsubscribe();
}
private void unsubscribe() throws ParseException, SipException, InvalidArgumentException {
Request request = this.subscriberDialog
.createRequest(Request.SUBSCRIBE);
ToHeader toHeader = (ToHeader)request.getHeader(ToHeader.NAME);
if (toHeader.getTag() == null) {
toHeader.setTag(subscriberToTag);
}
// Create a new MaxForwardsHeader
request.setHeader(headerFactory
.createMaxForwardsHeader(70));
// Create an event header for the subscription.
request.addHeader(headerFactory.createEventHeader("xcap-diff"));
// add expires header
request.setExpires(headerFactory.createExpiresHeader(0));
// create client transaction
ClientTransaction clientTransaction = sipProvider.getNewClientTransaction(request);
// send request
clientTransaction.sendRequest();
}
private void processTest4Notify(RequestEvent requestEvent) throws ParseException, SipException, InvalidArgumentException, JAXBException {
Request notify = requestEvent.getRequest();
javax.sip.message.Response response = messageFactory.createResponse(200, notify);
// SHOULD add a Contact
ContactHeader contact = (ContactHeader) contactHeader.clone();
((SipURI)contact.getAddress().getURI()).setParameter( "id", "sub" );
response.addHeader( contact );
requestEvent.getServerTransaction().sendResponse(response);
SubscriptionStateHeader subscriptionState = (SubscriptionStateHeader) requestEvent.getRequest().getHeader(SubscriptionStateHeader.NAME);
assertTrue("subscription didn't terminate", subscriptionState.getState().equalsIgnoreCase(SubscriptionStateHeader.TERMINATED));
}
// --- METHODS TO PROCESS SIP REQUESTS AND RESPONSES
public void processRequest(RequestEvent requestEvent) {
System.out.println("Request rcvd {\n"+requestEvent.getRequest()+"\n}");
Request request = requestEvent.getRequest();
if (request.getMethod().equals(Request.NOTIFY)) {
try {
switch (testRunning) {
case test1:
processTest1Notify(requestEvent);
semaphore.acquire();
doTest2();
semaphore.release();
break;
case test2:
semaphore.acquire();
processTest2Notify(requestEvent);
doTest3();
semaphore.release();
break;
case test3:
processTest3Notify(requestEvent);
doTest4();
break;
case test4:
processTest4Notify(requestEvent);
setTestResult(true);
break;
default:
System.err.println("unknown test");
setTestResult(false);
}
}
catch(Exception e) {
e.printStackTrace();
setTestResult(false);
}
}
}
public void processResponse(ResponseEvent responseReceivedEvent) {
System.out.println("Response received:\n" + responseReceivedEvent.getResponse());
assertTrue("received response to subscribe which signals that subscription was not approved", responseReceivedEvent.getResponse().getStatusCode() == 200);
if (subscriberToTag == null) {
subscriberToTag = ((ToHeader)responseReceivedEvent.getResponse().getHeader(ToHeader.NAME)).getTag();
}
}
// --- UNUSED SIP LISTENER METHODS
public void processDialogTerminated(DialogTerminatedEvent arg0) {
// TODO Auto-generated method stub
}
public void processIOException(IOExceptionEvent arg0) {
throw new RuntimeException("processIOException(IOExceptionEvent"+arg0+")");
}
public void processTimeout(TimeoutEvent arg0) {
throw new RuntimeException("processTimeout(TimeoutEvent="+arg0+")");
}
public void processTransactionTerminated(TransactionTerminatedEvent arg0) {
// TODO Auto-generated method stub
}
// ---- TEST setup/cleanup
@Before
public void runBefore() throws IOException, InterruptedException, MalformedObjectNameException, InstanceNotFoundException, NullPointerException, MBeanException, ReflectionException, NamingException {
super.runBefore();
try {
// init sip stack
notifierPort = "5060";
transport = "udp";
SipFactory sipFactory = SipFactory.getInstance();
sipFactory.setPathName("gov.nist");
Properties properties = new Properties();
properties.setProperty("javax.sip.USE_ROUTER_FOR_ALL_URIS", "false");
properties.setProperty("javax.sip.STACK_NAME", "subscriber");
properties.setProperty("gov.nist.javax.sip.DEBUG_LOG",
"subscriberdebug.txt");
properties.setProperty("gov.nist.javax.sip.SERVER_LOG",
"subscriberlog.txt");
properties.setProperty("javax.sip.FORKABLE_EVENTS", "foo");
// Set to 0 in your production code for max speed.
// You need 16 for logging traces. 32 for debug + traces.
// Your code will limp at 32 but it is best for debugging.
properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "0");
sipStack = sipFactory.createSipStack(properties);
logger.info("createSipStack " + sipStack);
headerFactory = sipFactory.createHeaderFactory();
addressFactory = sipFactory.createAddressFactory();
messageFactory = sipFactory.createMessageFactory();
this.listeningPoint = sipStack.createListeningPoint(ServerConfiguration.SERVER_HOST, 6060,
transport);
sipProvider = sipStack.createSipProvider(listeningPoint);
sipProvider.addSipListener(this);
}
catch (Exception e) {
throw new RuntimeException("Unable to start sip stack. Exception msg: "+e.getMessage());
}
}
@After
public void runAfter() throws IOException, InstanceNotFoundException, MBeanException, ReflectionException {
super.runAfter();
try {
sipProvider.removeSipListener(this);
sipStack.deleteSipProvider(sipProvider);
sipStack.deleteListeningPoint(listeningPoint);
sipStack.stop();
}
catch (Exception e) {
throw new RuntimeException("Unable to stop sip stack. Exception msg: "+e.getMessage());
}
}
private boolean passed = false;
/**
* sets test result
* @param result
*/
protected void setTestResult(boolean result) {
passed = result;
synchronized (this) {
this.notifyAll();
}
}
// ------ JAXB resource lists and xcap diff context
protected static final JAXBContext jaxbContext = initJAXBContext();
private static JAXBContext initJAXBContext() {
try {
return JAXBContext.newInstance(
"org.openxdm.xcap.common.xcapdiff" +
":org.openxdm.xcap.client.appusage.resourcelists.jaxb");
} catch (JAXBException e) {
logger.error("failed to create jaxb context");
return null;
}
}
}