/*
* 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 javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.ResponseEvent;
import javax.sip.SipProvider;
import javax.sip.TimeoutEvent;
import javax.sip.address.Address;
import javax.sip.header.AcceptHeader;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.EventHeader;
import javax.sip.header.SubscriptionStateHeader;
import javax.sip.header.SupportedHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import org.cafesip.jiplet.Jiplet;
import org.cafesip.jiplet.JipletLogger;
import org.cafesip.jiplet.JipletTransaction;
/**
* @author Becky McElroy
*
*/
public class Subscription
{
private String subscriptionId;
private String eventId;
private String eventType;
private Address subscribingParty;
private Address presentity;
private String toTag;
private Dialog dialog;
private SipProvider provider;
private String subscriptionState = SubscriptionStateHeader.ACTIVE;
private String terminationReason = "";
private long projectedExpiry = 0;
private Jiplet parent;
private SubscriptionList subscriptionList;
private JipletTransaction transaction;
public Subscription(Jiplet parent, SubscriptionList list,
String subscriptionId, String eventId, String eventType,
Address from, Address to, String toTag)
{
this.parent = parent;
this.subscriptionList = list;
this.subscriptionId = subscriptionId;
this.eventId = eventId;
this.eventType = eventType;
this.subscribingParty = from;
this.presentity = to;
this.toTag = toTag;
}
public void dispose()
{
if (parent.isDebugEnabled() == true)
{
parent.debug("Subscription: Disposing Subscription with ID "
+ subscriptionId + " from "
+ subscribingParty.getURI().toString());
}
if (transaction != null)
{
parent.cancelResponse(transaction);
transaction = null;
}
}
public void sendNotify(SipProvider provider)
{
ContactInfo reg_info = (ContactInfo) LocationDatabase.getInstance()
.get(dialog.getLocalParty().getURI().toString());
sendNotify(provider, reg_info);
}
public void sendNotify()
{
sendNotify(this.provider);
}
public void sendNotify(ContactInfo reg_info)
{
sendNotify(this.provider, reg_info);
}
public void sendNotify(SipProvider provider, ContactInfo reg_info)
// call setTimeLeft() or updateTimeLeft() before calling this method
{
if (provider == null)
{
parent.error("Null SipProvider, can't send NOTIFY");
return;
}
this.provider = provider;
if (parent.isDebugEnabled() == true)
{
parent.debug("Subscription: Creating NOTIFY message.");
}
try
{
Request req = dialog.createRequest(Request.NOTIFY);
// later - handle challenges, add accumulated authorization hdrs
// here
EventHeader ehdr = parent.getHeaderFactory().createEventHeader(
eventType);
if (eventId != null)
{
ehdr.setEventId(eventId);
}
req.addHeader(ehdr);
String state = getSubscriptionState();
SubscriptionStateHeader hdr = parent.getHeaderFactory()
.createSubscriptionStateHeader(state);
if (state.equals(SubscriptionStateHeader.TERMINATED))
{
hdr.setReasonCode(getTerminationReason());
}
else if (state.equals(SubscriptionStateHeader.ACTIVE)
|| state.equals(SubscriptionStateHeader.PENDING))
{
hdr.setExpires(getTimeLeft());
}
req.addHeader(hdr);
AcceptHeader accept = parent.getHeaderFactory().createAcceptHeader(
"application", "pidf+xml");
req.addHeader(accept);
SupportedHeader supported = parent.getHeaderFactory()
.createSupportedHeader("ms-benotify"); // for messenger -
// needed?
req.addHeader(supported);
// now for the body
String body = "";
// if (subscriptionList.findPublishedInfo(to.toString())) // use
// PUBLISH message info
{
}
// else
{
// build the body that contains the presence information
PresenceDocument doc = new PresenceDocument();
doc.setPresentity(presentity.getURI().toString());
PresenceTuple info = new PresenceTuple();
info.setId("1"); // later, support more than one
if ((reg_info == null)
|| (state.equals(SubscriptionStateHeader.PENDING)))
{
info.setStatus("closed");
info.setSubStatus("offline");
}
else
{
info.setContactInfo(reg_info);
info.setStatus("open");
info.setSubStatus("online");
}
doc.addTuple(info);
body = doc.encode();
}
ContentTypeHeader ct_hdr = parent.getHeaderFactory()
.createContentTypeHeader("application", "pidf+xml");
req.setContent(body, ct_hdr);
req.setContentLength(parent.getHeaderFactory()
.createContentLengthHeader(body.length()));
/*
*
* The NOTIFY request MAY contain a body indicating the state of the
* presentity. The means by which registration state is converted
* into presence state is a matter of local policy, and beyond the
* scope of this specification. However, some general guidelines can
* be provided. The address-of-record in the registration (the To
* header field) identifies the presentity. Each registered Contact
* header field identifies a point of communications for that
* presentity, which can be modeled using a tuple. Note that the
* contact address in the tuple need not be the same as the
* registered contact address. Using an address-of-record instead
* allows subsequent communications from a watcher to pass through
* proxies. This is useful for policy processing on behalf of the
* presentity and the provider.
*
* A PUA that uses registrations to manipulate presence state SHOULD
* make use of the SIP callee capabilities extension [9]. This
* allows the PUA to provide the PA with richer information about
* itself. For example, the presence of the methods parameter
* listing the method "MESSAGE" indicates support for instant
* messaging.
*
* later, when we suport multiple contact info's: The q values from
* the Contact header field [1] can be used to establish relative
* priorities amongst the various communications addresses in the
* Contact header fields. see RFC 3859 (Section 4), RFC 2779
* (5.1-5.3, 8.2)
*/
/*
* later: For reasons of privacy, it will frequently be necessary to
* encrypt the contents of the notifications. This can be
* accomplished using S/MIME. The encryption can be performed using
* the key of the subscriber as identified in the From field of the
* SUBSCRIBE request. Similarly, integrity of the notifications is
* important to subscribers. As such, the contents of the
* notifications MAY provide authentication and message integrity
* using S/MIME. Since the NOTIFY is generated by the presence
* server, which may not have access to the key of the user
* represented by the presentity, it will frequently be the case
* that the NOTIFY is signed by a third party. It is RECOMMENDED
* that the signature be by an authority over the domain of the
* presentity. In other words, for a user pres:user@example.com, the
* signator of the NOTIFY SHOULD be the authority for example.com.
*/
if (parent.isDebugEnabled() == true)
{
parent.debug("Subscription: Sending NOTIFY: " + req.toString());
}
ClientTransaction trans = provider.getNewClientTransaction(req);
if (state.equals(SubscriptionStateHeader.TERMINATED) == false)
{
transaction = parent.registerForResponse(req, 5000);
transaction.setAttribute("subscription", subscriptionId);
}
dialog.sendRequest(trans);
if (parent.isDebugEnabled() == true)
{
parent.debug("Subscription: Sent NOTIFY to "
+ subscribingParty.getURI().toString()
+ " for Subscription ID " + subscriptionId);
}
if (state.equals(SubscriptionStateHeader.TERMINATED) == true)
{
transaction = null;
subscriptionList.removeSubscription(this);
dispose();
}
}
catch (Exception e)
{
if (transaction != null)
{
parent.cancelResponse(transaction);
transaction = null;
}
parent.error(e.getClass().getName() + ": " + e.getMessage() + "\n"
+ JipletLogger.getStackTrace(e));
}
}
public void processResponse(ResponseEvent responseEvent)
{
int status = responseEvent.getResponse().getStatusCode();
if (parent.isDebugEnabled() == true)
{
parent.debug("Subscription: Processing " + status
+ " response for Subscription with ID " + subscriptionId
+ " from " + subscribingParty.getURI().toString());
}
if (status / 100 == 1)
{
return; // provisional response, keep waiting
}
transaction = null;
if ((status == Response.UNAUTHORIZED)
|| (status == Response.PROXY_AUTHENTICATION_REQUIRED))
{
// later - modify the request to include user authorization info and
// resend here
// (new JipletTransaction)
parent
.warn("Received authentication challenge at NOTIFY sending - not yet implemented");
return;
}
if (status == Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST)
{
// If a NOTIFY request receives a 481 response, the notifier MUST
// remove the corresponding subscription
subscriptionList.removeSubscription(this);
dispose();
return;
}
// later: A NOTIFY request is considered failed if
// a non-200 class response code is received which has no
// "Retry-After" header and no implied further action which can be taken
// to retry the request.
if (getSubscriptionState().equals(SubscriptionStateHeader.TERMINATED) == true)
{
subscriptionList.removeSubscription(this);
dispose();
}
}
public void processTimeout(TimeoutEvent timeout)
{
// this method is called if there was no response to the NOTIFY request
// we sent
transaction = null;
if (parent.isDebugEnabled() == true)
{
parent
.debug("Subscription: Processing NOTIFY response timeout for Subscription with ID "
+ subscriptionId
+ " from "
+ subscribingParty.getURI().toString());
}
if (getSubscriptionState().equals(SubscriptionStateHeader.TERMINATED) == true)
{
subscriptionList.removeSubscription(this);
dispose();
return;
}
setTimeLeft(0);
setSubscriptionState(SubscriptionStateHeader.TERMINATED);
sendNotify();
}
public static String determineEventId(Request request) // assumes event
// header
// already validated
{
EventHeader ev = (EventHeader) request.getHeader(EventHeader.NAME);
String event_id = ev.getEventId();
if (event_id == null)
{
event_id = "";
}
return event_id;
}
/**
* @return Returns the subscriptionState.
*/
public String getSubscriptionState()
{
return subscriptionState;
}
/**
* @param subscriptionState
* The subscriptionState to set.
*/
public void setSubscriptionState(String state)
{
this.subscriptionState = state;
}
/**
* @return Returns the timeLeft in seconds.
*/
public int getTimeLeft()
{
if (projectedExpiry == 0)
{
return 0;
}
return (int) ((projectedExpiry - System.currentTimeMillis()) / 1000);
}
/**
* @param timeLeft
* The timeLeft to set, in seconds.
*/
public synchronized void setTimeLeft(int timeLeft)
{
if (timeLeft <= 0)
{
if ((terminationReason == null)
|| (terminationReason.length() == 0))
{
setTerminationReason(SubscriptionStateHeader.TIMEOUT);
}
projectedExpiry = 0;
return;
}
projectedExpiry = System.currentTimeMillis() + (timeLeft * 1000);
}
public void updateTimeLeft()
{
setTimeLeft(getTimeLeft());
}
/**
* @return Returns the dialog.
*/
public Dialog getDialog()
{
return dialog;
}
/**
* @param dialog
* The dialog to set.
*/
public void setDialog(Dialog dialog)
{
this.dialog = dialog;
}
/**
* @return Returns the terminationReason.
*/
public String getTerminationReason()
{
return terminationReason;
}
/**
* @param terminationReason
* The terminationReason to set.
*/
public void setTerminationReason(String terminationReason)
{
this.terminationReason = terminationReason;
}
public String getSubscriptionId()
{
return subscriptionId;
}
public void setSubscriptionId(String subscriptionId)
{
this.subscriptionId = subscriptionId;
}
public String getToTag()
{
return toTag;
}
public Address getSubscribingParty()
{
return subscribingParty;
}
public Address getPresentity()
{
return presentity;
}
}