/*
* Created on July 7, 2005
*
* Copyright 2005 CafeSip.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.cafesip.reference.jiplet;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import javax.sip.Dialog;
import javax.sip.RequestEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipProvider;
import javax.sip.TransactionAlreadyExistsException;
import javax.sip.header.AcceptHeader;
import javax.sip.header.AllowEventsHeader;
import javax.sip.header.ContactHeader;
import javax.sip.header.EventHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.Header;
import javax.sip.header.SubscriptionStateHeader;
import javax.sip.header.SupportedHeader;
import javax.sip.header.ToHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import org.cafesip.jiplet.Jiplet;
import org.cafesip.jiplet.JipletException;
import org.cafesip.jiplet.JipletLogger;
import org.cafesip.jiplet.JipletRequest;
import org.cafesip.jiplet.JipletResponse;
import org.cafesip.jiplet.JipletTimeout;
import org.cafesip.jiplet.JipletTimer;
import org.cafesip.jiplet.JipletTransaction;
import org.cafesip.jiplet.Pair;
import org.cafesip.jiplet.TimerEvent;
import org.cafesip.jiplet.config.jip.JipletConfig;
/**
* @author Becky McElroy
*
*/
public class SipPresence extends Jiplet
{
private static int DEFAULT_SUBSCRIBE_DURATION = 3600; // seconds
private static long SUBS_EXPIRY_CHECK_TIMER = 10000; // msec
private SubscriptionList subscriptionList = new SubscriptionList();
// timer that wakes up every so often and checks for expired subscriptions
private TimerEvent timer = null;
public void init(JipletConfig config) throws JipletException
{
info(" being initialized");
}
public void destroy()
{
if (timer != null)
{
cancelTimer(timer);
timer = null;
}
subscriptionList.dispose();
}
public void doExtension(JipletRequest request)
{
RequestEvent event = request.getRequestEvent();
try
{
if (event.getRequest().getMethod().equals("PUBLISH") == true)
{
if (isDebugEnabled() == true)
{
debug("SipPresence: Received PUBLISH message: \n"
+ event.getRequest().toString());
}
// check event type, store this message in subscriptionList for
// presentity status, ignore if cseq <= last one from this
// subscriber
// can use this info when sending notify (if pidf), also remove
// from
// subscriptionList
// when presentity unregisters
try
{
Response response = getMessageFactory().createResponse(
Response.OK, event.getRequest());
sendResponse(event, response);
}
catch (Exception e)
{
error("A PUBLISH message could not be processed because an exception occured.\n"
+ e.getClass().getName()
+ ": "
+ e.getMessage()
+ "\n" + JipletLogger.getStackTrace(e));
}
}
}
finally
{
forward(request, "ExampleSIPRecorderJiplet");
}
}
public void doSubscribe(JipletRequest requestEvent)
{
if (timer == null)
{
timer = startTimer(SUBS_EXPIRY_CHECK_TIMER, true, requestEvent,
null);
}
try
{
RequestEvent event = requestEvent.getRequestEvent();
Request request = event.getRequest();
if (isDebugEnabled() == true)
{
debug("SipPresence: Received SUBSCRIBE: " + request);
}
// look at the event header first
EventHeader ev_hdr = (EventHeader) request
.getHeader(EventHeader.NAME);
if (eventHeaderValid(request, ev_hdr) == false)
{
// 489 Bad Event, debug msg already output
Response response = getMessageFactory().createResponse(
Response.BAD_EVENT, request);
sendResponse(event, response);
return;
}
if (ev_hdr.getEventType().equals("presence.winfo") == true)
{
// watcher info placeholder - we don't support this package
Response response = getMessageFactory().createResponse(
Response.BAD_EVENT, request);
sendResponse(event, response);
return;
}
// determine the duration
int duration = DEFAULT_SUBSCRIBE_DURATION;
ExpiresHeader exp = (ExpiresHeader) request
.getHeader(ExpiresHeader.NAME);
if (exp != null)
{
duration = exp.getExpires();
}
// find the active subscription (if exists) for this message
Subscription sub = subscriptionList.findSubscription(request);
if (sub != null)
{
if (duration > 0)
{
refresh(event, sub, duration);
return;
}
// this is an unsubscribe
try
{
unsubscribe(event, sub, duration);
}
catch (Exception e)
{
error("A SUBSCRIBE message could not be processed because an exception occured.\n"
+ e.getClass().getName()
+ ": "
+ e.getMessage()
+ "\n" + JipletLogger.getStackTrace(e));
}
// drop the subscription
subscriptionList.removeSubscription(sub);
sub.dispose();
return;
}
// new subscription
int response_code = Response.OK;
String reason = "OK";
String state = SubscriptionStateHeader.ACTIVE;
// send the response right away
String to_tag = new Long(Calendar.getInstance().getTimeInMillis())
.toString();
Response response = createResponse(response_code, reason, to_tag,
request, duration);
ServerTransaction transaction = sendResponse(event, response);
if (transaction == null)
{
// stack couldn't give us a dialog
return;
}
Dialog dialog = transaction.getDialog();
sub = new Subscription(this, subscriptionList, SubscriptionList
.getSubscriptionId(request), ev_hdr.getEventId(), ev_hdr
.getEventType(), ((FromHeader) request
.getHeader(FromHeader.NAME)).getAddress(),
((ToHeader) request.getHeader(ToHeader.NAME)).getAddress(),
to_tag);
sub.setDialog(dialog);
sub.setSubscriptionState(state);
sub.setTimeLeft(duration);
if (duration == 0) // it's a fetch or contact was removed from the
// phone
{
sub.setTerminationReason("Presence Fetch");
sub.setSubscriptionState(SubscriptionStateHeader.TERMINATED);
}
else
{
subscriptionList.addSubscription(sub);
}
sub.sendNotify((SipProvider) event.getSource());
}
catch (Exception e)
{
error("A SUBSCRIBE message could not be processed because an exception occured.\n"
+ e.getClass().getName()
+ ": "
+ e.getMessage()
+ "\n"
+ JipletLogger.getStackTrace(e));
}
finally
{
forward(requestEvent, "ExampleSIPRecorderJiplet");
}
// later: conversion: request URI, from, to can be: pres, sip, or
// sips
// later: note the Accept Header for Notification sending - The
// presence of the "Allow-Events" header in a message is sufficient
// to indicate support for NOTIFY. The "methods" parameter for
// Contact may also be used to specifically announce support for
// NOTIFY messages when registering.
}
private void unsubscribe(RequestEvent event, Subscription sub, int duration)
throws Exception
{
int response_code = Response.OK;
String reason = "OK";
// set subscription state
sub.setSubscriptionState(SubscriptionStateHeader.TERMINATED);
sub.setTerminationReason("unsubscribed");
// send the response
Response response = createResponse(response_code, reason, sub
.getToTag(), event.getRequest(), duration);
ServerTransaction transaction = sendResponse(event, response);
// send NOTIFY - why dialog state terminated by this time? Stack is
// doing it?
if (transaction != null)
{
// / sub.sendNotify((SipProvider) event.getSource());
}
}
private void refresh(RequestEvent event, Subscription sub, int duration)
throws Exception
{
int response_code = Response.ACCEPTED;
String reason = "Accepted";
if (sub.getSubscriptionState().equals(SubscriptionStateHeader.ACTIVE) == true)
{
response_code = Response.OK;
reason = "OK";
}
else if (sub.getSubscriptionState().equals(
SubscriptionStateHeader.TERMINATED) == true)
{
reason = sub.getTerminationReason();
if (reason.equals("blocked"))
{
response_code = Response.DECLINE;
}
else if (reason.equals("timeout"))
{
response_code = Response.OK;
}
}
// send the response
Response response = createResponse(response_code, reason, sub
.getToTag(), event.getRequest(), duration);
ServerTransaction transaction = sendResponse(event, response);
// update time left
sub.setTimeLeft(duration);
// send NOTIFY
if (transaction != null)
{
sub.sendNotify((SipProvider) event.getSource());
}
}
private boolean eventHeaderValid(Request request, EventHeader ev_hdr)
{
if (ev_hdr == null)
{
if (isDebugEnabled() == true)
{
error("Received a SUBSCRIBE message with no event header from "
+ ((FromHeader) request.getHeader(FromHeader.NAME))
.getAddress().getURI().toString());
}
return false;
}
if ((ev_hdr.getEventType().equals("presence") == false)
&& (ev_hdr.getEventType().equals("presence.winfo") == false)) // watcher
// info
{
if (isDebugEnabled() == true)
{
debug("SipPresence: Received non-presence SUBSCRIBE from "
+ ((FromHeader) request.getHeader(FromHeader.NAME))
.getAddress().getURI().toString());
}
return false;
}
return true;
}
private Response createResponse(int statusCode, String reasonPhrase,
String toTag, Request request, int duration) throws Exception
{
Response response = getMessageFactory().createResponse(statusCode,
request);
response.addHeader(getHeaderFactory().createExpiresHeader(duration));
response.setReasonPhrase(reasonPhrase);
ToHeader to = (ToHeader) response.getHeader(ToHeader.NAME);
to.setTag(toTag);
if (statusCode / 100 == 2) // 2xx
{
ContactInfo reg_info = (ContactInfo) LocationDatabase.getInstance()
.get(to.getAddress().getURI().toString());
ContactHeader c = null;
if (reg_info != null)
{
c = reg_info.getContact();
}
if (isDebugEnabled() == true)
{
debug("SipPresence: creating response with contact header = "
+ c);
debug(" got reg_info for URI "
+ to.getAddress().getURI().toString());
debug(" reg_info = " + reg_info);
}
if (c != null)
{
response.setHeader(c);
}
AcceptHeader accept = getHeaderFactory().createAcceptHeader(
"application", "pidf+xml");
response.addHeader(accept);
SupportedHeader supported = getHeaderFactory()
.createSupportedHeader("ms-benotify"); // for
// messenger
response.addHeader(supported);
SupportedHeader shdr = getHeaderFactory().createSupportedHeader(
"presence");
response.addHeader(shdr);
Header hdr = getHeaderFactory().createHeader("ms-keep-alive",
"UAS; tcp=yes; hop-hop=no; end-end=yes; timeout=600");
response.addHeader(hdr);
AllowEventsHeader ahdr = getHeaderFactory()
.createAllowEventsHeader("presence");
response.addHeader(ahdr);
}
return response;
}
private ServerTransaction sendResponse(RequestEvent event, Response response)
throws Exception
{
ServerTransaction trans = event.getServerTransaction();
if (trans == null)
{
try
{
trans = ((SipProvider) event.getSource())
.getNewServerTransaction(event.getRequest());
}
catch (TransactionAlreadyExistsException e)
{
if (isDebugEnabled() == true)
{
debug("SipPresence: SipPresence.sendResponse() -- no existing transaction, cannot get a new one - sending response statelessly.");
}
((SipProvider) event.getSource()).sendResponse(response);
return null;
}
}
if (trans == null)
{
if (isDebugEnabled() == true)
{
debug("SipPresence: SipPresence.sendResponse() -- no existing transaction, get new returned null - sending response statelessly.");
}
((SipProvider) event.getSource()).sendResponse(response);
return null;
}
trans.sendResponse(response);
return trans;
}
/*
* @see org.cafesip.jiplet.Jiplet#processResponse(org.cafesip.jiplet.JipletResponse)
*/
public void processResponse(JipletResponse response)
{
super.processResponse(response);
try
{
JipletTransaction trans = response.getTransaction();
Subscription s = subscriptionList.findSubscription((String) trans
.getAttribute("subscription"));
if (s != null)
{
s.processResponse(response.getResponseEvent());
}
}
finally
{
forward(response, "ExampleSIPRecorderJiplet");
}
}
/*
* @see org.cafesip.jiplet.Jiplet#processTimeout(org.cafesip.jiplet.JipletTimeout)
*/
public void processTimeout(JipletTimeout timeout)
{
try
{
// this method is called if there was no response to the NOTIFY
// request
// we sent
super.processTimeout(timeout);
JipletTransaction trans = timeout.getTransaction();
Subscription s = subscriptionList.findSubscription((String) trans
.getAttribute("subscription"));
if (s != null)
{
s.processTimeout(timeout.getTimeoutEvent());
}
}
finally
{
forward(timeout, "ExampleSIPRecorderJiplet");
}
}
public void processTimer(JipletTimer timer)
{
// are we here because of expired registration(s)?
ArrayList expired = (ArrayList) timer
.getAttribute("expiredRegistrations");
if (expired != null)
{
Iterator i = expired.iterator();
while (i.hasNext())
{
processRegistration((String) i.next(), null);
}
return;
}
// no, we are here to audit subscriptions - check for expired ones
expired = subscriptionList.collectExpiredSubscriptions();
// end the expired subscriptions
Iterator i = expired.iterator();
while (i.hasNext())
{
Subscription s = (Subscription) i.next();
if (isDebugEnabled() == true)
{
debug("SipPresence: Subscription has expired. Idling Subscription with ID "
+ s.getSubscriptionId()
+ " from "
+ s.getSubscribingParty().getURI().toString());
}
s.sendNotify();
}
}
/*
* @see org.cafesip.jiplet.Jiplet#doRegister(javax.sip.RequestEvent)
*/
public void doRegister(JipletRequest requestEvent)
{
if (isDebugEnabled() == true)
{
debug("SipPresence: received REGISTER event");
}
try
{
Pair status = (Pair) requestEvent.getAttribute("registrationInfo");
if (status == null)
{
error("SipPresence.doRegister() - SCOPE VARIABLE STATUS IS NULL - cannot notify buddies of registration/unregistration.");
return;
}
processRegistration((String) status.getFirst(),
(ContactInfo) status.getSecond());
}
finally
{
forward(requestEvent, "ExampleSIPRecorderJiplet");
}
return;
}
private void processRegistration(String registree, ContactInfo reg_info)
{
if (isDebugEnabled() == true)
{
if (reg_info != null)
{
debug("SipPresence: new registree = " + registree
+ ", contact addr = "
+ reg_info.getContact().toString() + ", expiry = "
+ reg_info.getExpiryTime().toString());
}
else
{
debug("SipPresence: registree logged out: " + registree);
}
}
registree = getActualAddress(registree);
// who is tracking the registree? Need to notify them.
subscriptionList.notifyWatchers(registree, reg_info);
// if this is an unregistration, idle any subscriptions the registree
// has going
if (reg_info == null)
{
subscriptionList.idleUserSubscriptions(registree);
}
}
private String getActualAddress(String uri)
{
// an address can be of the form sip:name@domain:port;transport=x
// We only want to store up to sip:name@domain
int index = uri.indexOf(":", 4);
if (index >= 0)
{
return uri.substring(0, index);
}
index = uri.indexOf(";");
if (index >= 0)
{
return uri.substring(0, index);
}
return uri;
}
}