package com.voxeo.moho.presence.sip.impl; import java.util.HashMap; import java.util.Map; import javax.servlet.sip.SipServletRequest; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.voxeo.moho.event.AcceptableEvent; import com.voxeo.moho.event.AcceptableEvent.Reason; import com.voxeo.moho.event.SubscribeEvent.SubscriptionContext; import com.voxeo.moho.presence.NotifyBody; import com.voxeo.moho.presence.NotifyDispatcher; import com.voxeo.moho.presence.Resource; import com.voxeo.moho.presence.StoreRetrieveListener; import com.voxeo.moho.presence.SubscriptionState; import com.voxeo.moho.presence.impl.StoreHolder; import com.voxeo.moho.presence.impl.sip.SIPPresenceStore; import com.voxeo.moho.presence.impl.sip.memory.SIPMemoryPresenceStore; import com.voxeo.moho.presence.sip.EventSoftState; import com.voxeo.moho.presence.sip.SIPPresenceResource; import com.voxeo.moho.presence.sip.SIPPresenceService; import com.voxeo.moho.presence.sip.SIPResource; import com.voxeo.moho.presence.sip.SipPresenceFactory; import com.voxeo.moho.sip.SIPPublishEvent; import com.voxeo.moho.sip.SIPSubscribeEvent; import com.voxeo.moho.sip.SIPSubscribeEvent.SIPSubscriptionContext; import com.voxeo.moho.spi.ExecutionContext; public class SIPPresenceServiceImpl implements SIPPresenceService { private final static Logger LOG = Logger.getLogger(SIPPresenceServiceImpl.class); private ExecutionContext _context; private SIPPresenceStore _store; private SipPresenceFactory _presenceFactory; private NotifyDispatcher _notifyDispatcher; @Override public void init(ExecutionContext context, Map<String, String> props) { _context = context; _presenceFactory = _context.getService(SipPresenceFactory.class); String storeImpl = props.get(STORE_IMPL); if (storeImpl == null) { storeImpl = SIPMemoryPresenceStore.class.getName(); } try { _store = (SIPPresenceStore) Class.forName(storeImpl).newInstance(); _store.init(props); _store.addRetrieveListener(Resource.class, new StoreRetrieveListener<Resource>() { @Override public void onRetrieve(Resource resource) { resource.setExecutionContext(_context); } }); _store.addRetrieveListener(SubscriptionContext.class, new StoreRetrieveListener<SubscriptionContext>() { @Override public void onRetrieve(SubscriptionContext resource) { resource.setExecutionContext(_context); } }); } catch (Exception e) { throw new IllegalArgumentException("Invalidate Presence Store implementation: " + e); } int capacity = 1000; _notifyDispatcher = new MemoryNotifyDispatcher(context.getExecutor(), capacity); context.getExecutor().execute(_notifyDispatcher); context.getExecutor().execute(this); } @Override public void doPublish(SIPPublishEvent event) { try { SipServletRequest req = event.getSipRequest(); // validate expire int expires = req.getExpires(); if (_presenceFactory.checkPublishExpires(expires) < 0) { rejectMinExpire(event, _presenceFactory); } byte[] content = req.getRawContent(); // for especially return byte[0] if (content != null && content.length == 0) { content = null; } StoreHolder.setPresenceStore(_store); // validate supported notifyBodyType // getResource final String resourceKey = Utils.getCleanUri(req.getTo().getURI()).toString(); //TODO refactor all request related staff to event level EventHeader eventHeader = new EventHeader(req.getHeader("Event")); SIPPresenceResource resource = (SIPPresenceResource) getResource(_presenceFactory, resourceKey, eventHeader.getEventName()); String contentType = req.getContentType(); if (StringUtils.isEmpty(contentType)) { contentType = _presenceFactory.getDefaultNotifyBodyName(eventHeader.getEventName()); } final String sipIfMatch = event.getSipRequest().getHeader("SIP-If-Match"); EventSoftState eventSoftState = null; // This is the first publish request if (content != null && sipIfMatch == null && expires > 0) { NotifyBody notifyBody = _presenceFactory.createNotifyBody(event.getSipRequest()); eventSoftState = new SIPEventSoftState(resourceKey, eventHeader.getEventName(), expires, contentType, notifyBody); resource.addEventSoftState(eventSoftState); acceptPublish(event, eventSoftState); } // Publish keepalive else if (content == null && sipIfMatch != null && expires > 0) { eventSoftState = resource.getSoftState(sipIfMatch); if (eventSoftState != null) { eventSoftState.updateExpires(expires); resource.refreshEventSoftState(eventSoftState); acceptPublish(event, eventSoftState); } else { event.reject(Reason.CONDITIONAL_REQUEST_FAILED); } } // publish modify else if (content != null && sipIfMatch != null && expires > 0) { NotifyBody notifyBody = _presenceFactory.createNotifyBody(event.getSipRequest()); eventSoftState = resource.getSoftState(sipIfMatch); if (eventSoftState != null) { eventSoftState.setBody(notifyBody); eventSoftState.updateExpires(expires); resource.refreshEventSoftState(eventSoftState); acceptPublish(event, eventSoftState); } else { event.reject(Reason.CONDITIONAL_REQUEST_FAILED); } } // publish removal else if (content == null && sipIfMatch != null && expires == 0) { eventSoftState = resource.getSoftState(sipIfMatch); if (eventSoftState != null) { resource.removeEventSoftState(eventSoftState); event.accept(); } else { event.reject(Reason.CONDITIONAL_REQUEST_FAILED); } } // bad request else { LOG.warn("Bad publish request[SIP-If-Match=" + sipIfMatch + ", expires=" + expires + ", hasContent=" + content != null); event.reject(Reason.BAD_REQUEST); } } catch (Exception e) { LOG.error("Error handling subscribe", e); event.reject(Reason.ERROR); } finally { StoreHolder.setPresenceStore(null); } } @Override public void doSubscribe(SIPSubscribeEvent event) { if (isResponsibleFor(event)) { try { SIPSubscriptionContext subscription = (SIPSubscriptionContext) event.getSubscription(); // TODO support resourcelist // verify event if (subscription.getEventName() == null || !SIPConstans.EVENT_NAME_PRESENCE.equals(subscription.getEventName())) { HashMap<String, String> headers = new HashMap<String, String>(); headers.put("Allow-Events", SIPConstans.EVENT_NAME_PRESENCE); event.reject(Reason.BAD_EVENT, headers); return; } // verify expires int expire = _presenceFactory.checkSubscribeExpires(subscription.getExpires()); if (expire < 0) { rejectMinExpire(event, _presenceFactory); } _store.startTx(); StoreHolder.setPresenceStore(_store); // get resource SIPResource resource = getResource(_presenceFactory, subscription.getSubscribee(), subscription.getEventName()); SubscriptionState state = null; // update subscription context if (_store.isSubscriptionExist(subscription)) { if (subscription.getExpires() == 0) { state = resource.removeSubscripton(subscription); } else { state = resource.updateSubscripton(subscription); } } else { state = resource.addSubscription(subscription); } _store.updateResource(resource); // send 200 OK/202 Accepted before send notify if (state == SipSubscriptionStateImpl.ALLOW || state == SipSubscriptionStateImpl.TERMINATED) { HashMap<String, String> props = new HashMap<String, String>(); props.put("Expire", String.valueOf(subscription.getExpires())); event.accept(props); if (LOG.isDebugEnabled()) { LOG.debug("Will send notify to " + subscription + " due to subscribe"); } // send notify Runnable sendNotify = subscription.sendNotify(); _notifyDispatcher.put((NotifyRequest) sendNotify); _store.commitTx(); } else { if (LOG.isDebugEnabled()) { LOG.debug("Decline " + subscription + " due to presence policy"); } event.reject(Reason.FORBIDEN); _store.rollbackTx(); } if (state == SipSubscriptionStateImpl.TERMINATED) { //FIXME do it when receiving the 200OK of NOTIFY invalidateApplicationSession(event.getSipRequest()); } } catch (Throwable e) { LOG.error("Handling subscribe error", e); event.reject(Reason.ERROR); _store.rollbackTx(); } finally { StoreHolder.setPresenceStore(null); } } else { // TODO proxy } } private boolean isResponsibleFor(SIPSubscribeEvent event) { // TODO check domains. return true; } private void invalidateApplicationSession(final SipServletRequest req) { if (req.getApplicationSession().isValid()) { req.getApplicationSession().invalidate(); } } private void acceptPublish(SIPPublishEvent event, EventSoftState eventSoftState) { HashMap<String, String> headers = new HashMap<String, String>(); headers.put("SIP-ETag", eventSoftState.getEntityTag()); headers.put("Expires", String.valueOf(eventSoftState.getSpareTime())); event.accept(headers); } private void rejectMinExpire(AcceptableEvent event, SipPresenceFactory presenceFactory) { HashMap<String, String> headers = new HashMap<String, String>(); headers.put("Min-Expires", String.valueOf(presenceFactory.getSubscribeMinExpires())); event.reject(Reason.INTERVAL_TOO_BRIEF, headers); } private SIPResource getResource(SipPresenceFactory presenceFactory, String resourceUri, String eventName) { SIPResource resource = (SIPResource) _store.getResource(resourceUri, eventName); if (resource == null) { resource = presenceFactory.createResource(resourceUri, eventName); if (LOG.isDebugEnabled()) { LOG.debug("Created " + resource); } _store.addResource(resource); } return resource; } @Override public void destroy() { _context = null; if (_notifyDispatcher != null) { _notifyDispatcher.shutdown(); } _notifyDispatcher = null; _presenceFactory = null; if (_store != null) { _store.destroy(); } _store = null; } @Override public void run() { // TODO EventSoftState clean // TODO Subscription clean } public NotifyDispatcher getNotifyDispatcher() { return _notifyDispatcher; } @Override public String getName() { return SIPPresenceService.class.getName(); } }