aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBoris Grozev <boris@jitsi.org>2013-06-27 18:42:20 +0300
committerBoris Grozev <boris@jitsi.org>2013-06-27 18:42:20 +0300
commit4643383af893c51664063fe249a024ca7b8398be (patch)
tree568e3d20aaeb9546fecbf226ab95f54409083187
parent906af90a0423ad03620c1b761b674958d9d31428 (diff)
parent6e56eb79858c869bb450d597ee08831b271828a1 (diff)
downloadjitsi-4643383af893c51664063fe249a024ca7b8398be.zip
jitsi-4643383af893c51664063fe249a024ca7b8398be.tar.gz
jitsi-4643383af893c51664063fe249a024ca7b8398be.tar.bz2
Merge branch 'coin-refactor'
-rw-r--r--src/net/java/sip/communicator/impl/protocol/jabber/CallPeerGTalkImpl.java8
-rw-r--r--src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java47
-rw-r--r--src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java303
-rw-r--r--src/net/java/sip/communicator/impl/protocol/jabber/jabber.provider.manifest.mf1
-rw-r--r--src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java10
-rw-r--r--src/net/java/sip/communicator/impl/protocol/sip/OperationSetTelephonyConferencingSipImpl.java416
-rw-r--r--src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java599
-rw-r--r--src/net/java/sip/communicator/service/protocol/media/ConferenceInfoDocument.java1282
-rw-r--r--src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java116
-rw-r--r--src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf4
10 files changed, 2238 insertions, 548 deletions
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerGTalkImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerGTalkImpl.java
index e648b18..d7cbc1f 100644
--- a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerGTalkImpl.java
+++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerGTalkImpl.java
@@ -658,4 +658,12 @@ public class CallPeerGTalkImpl
protocolProvider.getConnection().sendPacket(candidatesIQ);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getEntity()
+ {
+ return getAddress();
+ }
}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java
index 2bd1610..639a13f 100644
--- a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java
+++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java
@@ -72,6 +72,17 @@ public class CallPeerJabberImpl
private final Object sidSyncRoot = new Object();
/**
+ * Whether a COIN has been scheduled to be sent to this
+ * <tt>CallPeerJabberImpl</tt>
+ */
+ private boolean coinScheduled = false;
+
+ /**
+ * Synchronization object for coinScheduled
+ */
+ private final Object coinScheduledSyncRoot = new Object();
+
+ /**
* Creates a new call peer with address <tt>peerAddress</tt>.
*
* @param peerAddress the Jabber address of the new call peer.
@@ -1453,4 +1464,40 @@ public class CallPeerJabberImpl
new TransferredPacketExtension()));
}
}
+
+ /**
+ * Check whether a COIN is scheduled to be sent to this <tt>CallPeer</tt>
+ * (i.e. there is a thread which will eventually (after sleeping a certain
+ * amount of time) trigger a COIN to be sent)
+ * @return <tt>true</tt> if there is a COIN scheduled to be sent to this
+ * <tt>CallPeer</tt> and <tt>false</tt> otherwise
+ */
+ public boolean isCoinScheduled()
+ {
+ synchronized (coinScheduledSyncRoot)
+ {
+ return coinScheduled;
+ }
+ }
+
+ /**
+ * Sets the property which indicates whether a COIN is scheduled to be sent
+ * to this <tt>CallPeer</tt>.
+ * @param coinScheduled
+ */
+ public void setCoinScheduled(boolean coinScheduled)
+ {
+ synchronized (coinScheduledSyncRoot)
+ {
+ this.coinScheduled = coinScheduled;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getEntity()
+ {
+ return getAddress();
+ }
}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java
index 34ebaa5..9be9aee 100644
--- a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java
+++ b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java
@@ -15,7 +15,7 @@ import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.media.*;
import net.java.sip.communicator.util.*;
-import org.jitsi.service.neomedia.*;
+import org.jitsi.util.xml.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
@@ -27,6 +27,7 @@ import org.jivesoftware.smackx.packet.*;
*
* @author Lyubomir Marinov
* @author Sebastien Vincent
+ * @author Boris Grozev
*/
public class OperationSetTelephonyConferencingJabberImpl
extends AbstractOperationSetTelephonyConferencing<
@@ -49,15 +50,15 @@ public class OperationSetTelephonyConferencingJabberImpl
= Logger.getLogger(OperationSetTelephonyConferencingJabberImpl.class);
/**
- * Synchronization object.
+ * The minimum interval in milliseconds between COINs sent to a single
+ * <tt>CallPeer</tt>.
*/
- private final Object lock = new Object();
+ private static final int COIN_MIN_INTERVAL = 200;
/**
- * The value of the <tt>version</tt> attribute to be specified in the
- * outgoing <tt>conference-info</tt> root XML elements.
+ * Synchronization object.
*/
- private int version = 1;
+ private final Object lock = new Object();
/**
* Initializes a new <tt>OperationSetTelephonyConferencingJabberImpl</tt>
@@ -97,8 +98,6 @@ public class OperationSetTelephonyConferencingJabberImpl
{
notify(i.next());
}
-
- version++;
}
}
}
@@ -114,10 +113,41 @@ public class OperationSetTelephonyConferencingJabberImpl
if(!(callPeer instanceof CallPeerJabberImpl))
return;
+ final CallPeerJabberImpl callPeerJabber = (CallPeerJabberImpl)callPeer;
+
+ final long timeSinceLastCoin = System.currentTimeMillis()
+ - callPeerJabber.getLastConferenceInfoSentTimestamp();
+ if (timeSinceLastCoin < COIN_MIN_INTERVAL)
+ {
+ if (callPeerJabber.isCoinScheduled())
+ return;
+
+ logger.info("Scheduling to send a COIN to " + callPeerJabber);
+ callPeerJabber.setCoinScheduled(true);
+ new Thread(new Runnable(){
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(1 + COIN_MIN_INTERVAL - timeSinceLastCoin);
+ }
+ catch (InterruptedException ie) {}
+
+ OperationSetTelephonyConferencingJabberImpl.this
+ .notify(callPeerJabber);
+ }
+ }).start();
+
+ return;
+ }
+
// check that callPeer supports COIN before sending him a
// conference-info
String to = getBasicTelephony().getFullCalleeURI(callPeer.getAddress());
+ // XXX if this generates actual disco#info requests we might want to
+ // cache it.
try
{
DiscoverInfo discoverInfo
@@ -127,6 +157,7 @@ public class OperationSetTelephonyConferencingJabberImpl
ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_COIN))
{
logger.info(callPeer.getAddress() + " does not support COIN");
+ callPeerJabber.setCoinScheduled(false);
return;
}
}
@@ -135,135 +166,41 @@ public class OperationSetTelephonyConferencingJabberImpl
logger.warn("Failed to retrieve DiscoverInfo for " + to, xmppe);
}
- IQ iq = getConferenceInfo((CallPeerJabberImpl) callPeer, version);
+ ConferenceInfoDocument currentConfInfo
+ = getCurrentConferenceInfo(callPeerJabber);
+ ConferenceInfoDocument lastSentConfInfo
+ = callPeerJabber.getLastConferenceInfoSent();
- if (iq != null)
- parentProvider.getConnection().sendPacket(iq);
- }
+ ConferenceInfoDocument diff;
- /**
- * Get media packet extension for the specified <tt>CallPeerJabberImpl</tt>.
- *
- * @param callPeer <tt>CallPeer</tt>
- * @param remote if the callPeer is remote or local
- * @return list of media packet extension
- */
- private List<MediaPacketExtension> getMedia(
- MediaAwareCallPeer<?,?,?> callPeer,
- boolean remote)
- {
- CallPeerMediaHandler<?> mediaHandler = callPeer.getMediaHandler();
- List<MediaPacketExtension> ret = new ArrayList<MediaPacketExtension>();
- long i = 1;
+ if (lastSentConfInfo == null)
+ diff = currentConfInfo;
+ else
+ diff = getConferenceInfoDiff(lastSentConfInfo, currentConfInfo);
- for(MediaType mediaType : MediaType.values())
+ if (diff != null)
{
- MediaStream stream = mediaHandler.getStream(mediaType);
-
- if (stream != null)
- {
- MediaPacketExtension ext
- = new MediaPacketExtension(Long.toString(i));
- long srcId
- = remote
- ? getRemoteSourceID(callPeer, mediaType)
- : stream.getLocalSourceID();
+ int newVersion
+ = lastSentConfInfo == null
+ ? 1
+ : lastSentConfInfo.getVersion() + 1;
+ diff.setVersion(newVersion);
- if (srcId != -1)
- ext.setSrcID(Long.toString(srcId));
+ IQ iq = getConferenceInfo(callPeerJabber, diff);
- ext.setType(mediaType.toString());
-
- MediaDirection direction
- = remote
- ? getRemoteDirection(callPeer, mediaType)
- : stream.getDirection();
-
- if (direction == null)
- direction = MediaDirection.INACTIVE;
-
- ext.setStatus(direction.toString());
- ret.add(ext);
- i++;
- }
- }
-
- return ret;
- }
-
- /**
- * Get user packet extension for the specified <tt>CallPeerJabberImpl</tt>.
- *
- * @param callPeer <tt>CallPeer</tt>
- * @return user packet extension
- */
- private UserPacketExtension getUser(CallPeer callPeer)
- {
- UserPacketExtension ext
- = new UserPacketExtension(callPeer.getAddress());
-
- ext.setDisplayText(callPeer.getDisplayName());
-
- EndpointPacketExtension endpoint
- = new EndpointPacketExtension(callPeer.getURI());
-
- endpoint.setStatus(getEndpointStatus(callPeer));
-
- if (callPeer instanceof MediaAwareCallPeer<?,?,?>)
- {
- List<MediaPacketExtension> medias
- = getMedia((MediaAwareCallPeer<?,?,?>) callPeer, true);
-
- if(medias != null)
+ if (iq != null)
{
- for(MediaPacketExtension media : medias)
- endpoint.addChildExtension(media);
+ parentProvider.getConnection().sendPacket(iq);
+
+ // We save currentConfInfo, because it is of state "full", while
+ // diff could be a partial
+ currentConfInfo.setVersion(newVersion);
+ callPeerJabber.setLastConferenceInfoSent(currentConfInfo);
+ callPeerJabber.setLastConferenceInfoSentTimestamp(
+ System.currentTimeMillis());
}
}
-
- ext.addChildExtension(endpoint);
-
- return ext;
- }
-
- /**
- * Generates the text content to be put in the <tt>status</tt> XML element
- * of an <tt>endpoint</tt> XML element and which describes the state of a
- * specific <tt>CallPeer</tt>.
- *
- * @param callPeer the <tt>CallPeer</tt> which is to get its state described
- * in a <tt>status</tt> XML element of an <tt>endpoint</tt> XML element
- * @return the text content to be put in the <tt>status</tt> XML element of
- * an <tt>endpoint</tt> XML element and which describes the state of the
- * specified <tt>callPeer</tt>
- */
- private EndpointStatusType getEndpointStatus(CallPeer callPeer)
- {
- CallPeerState callPeerState = callPeer.getState();
-
- if (CallPeerState.ALERTING_REMOTE_SIDE.equals(callPeerState))
- return EndpointStatusType.alerting;
- if (CallPeerState.CONNECTING.equals(callPeerState)
- || CallPeerState
- .CONNECTING_WITH_EARLY_MEDIA.equals(callPeerState))
- return EndpointStatusType.pending;
- if (CallPeerState.DISCONNECTED.equals(callPeerState))
- return EndpointStatusType.disconnected;
- if (CallPeerState.INCOMING_CALL.equals(callPeerState))
- return EndpointStatusType.dialing_in;
- if (CallPeerState.INITIATING_CALL.equals(callPeerState))
- return EndpointStatusType.dialing_out;
-
- /*
- * he/she is neither "hearing" the conference mix nor is his/her media
- * being mixed in the conference
- */
- if (CallPeerState.ON_HOLD_LOCALLY.equals(callPeerState)
- || CallPeerState.ON_HOLD_MUTUALLY.equals(callPeerState))
- return EndpointStatusType.on_hold;
- if (CallPeerState.CONNECTED.equals(callPeerState))
- return EndpointStatusType.connected;
- return null;
+ callPeerJabber.setCoinScheduled(false);
}
/**
@@ -272,67 +209,34 @@ public class OperationSetTelephonyConferencingJabberImpl
* conference managed by the local peer.
*
* @param callPeer the <tt>CallPeer</tt> to generate conference-info XML for
- * @param version the value of the version attribute of the
- * <tt>conference-info</tt> root element of the conference-info XML to be
- * generated
+ * @param confInfo the <tt>ConferenceInformationDocument</tt> which is to be
+ * included in the IQ
* @return the conference-info IQ to be sent to the specified
* <tt>callPeer</tt> in order to notify it of the current state of the
* conference managed by the local peer
*/
- private IQ getConferenceInfo(CallPeerJabberImpl callPeer, int version)
+ private IQ getConferenceInfo(CallPeerJabberImpl callPeer,
+ final ConferenceInfoDocument confInfo)
{
String callPeerSID = callPeer.getSID();
if (callPeerSID == null)
return null;
- CoinIQ iq = new CoinIQ();
+ IQ iq = new IQ(){
+ @Override
+ public String getChildElementXML()
+ {
+ return confInfo.toXml();
+ }
+ };
+
CallJabberImpl call = callPeer.getCall();
iq.setFrom(call.getProtocolProvider().getOurJID());
iq.setTo(callPeer.getAddress());
iq.setType(Type.SET);
- iq.setEntity(getBasicTelephony().getProtocolProvider().getOurJID());
- iq.setVersion(version);
- iq.setState(StateType.full);
- iq.setSID(callPeerSID);
-
- // conference-description
- iq.addExtension(new DescriptionPacketExtension());
-
- // conference-state
- StatePacketExtension state = new StatePacketExtension();
- List<CallPeer> conferenceCallPeers = CallConference.getCallPeers(call);
-
- state.setUserCount(
- 1 /* the local peer/user */ + conferenceCallPeers.size());
- iq.addExtension(state);
-
- // users
- UsersPacketExtension users = new UsersPacketExtension();
-
- // user
- UserPacketExtension user
- = new UserPacketExtension("xmpp:" + parentProvider.getOurJID());
- // endpoint
- EndpointPacketExtension endpoint = new EndpointPacketExtension(
- "xmpp:" + parentProvider.getOurJID());
- endpoint.setStatus(EndpointStatusType.connected);
-
- // media
- List<MediaPacketExtension> medias = getMedia(callPeer, false);
-
- for(MediaPacketExtension media : medias)
- endpoint.addChildExtension(media);
- user.addChildExtension(endpoint);
- users.addChildExtension(user);
-
- // other users
- for (CallPeer conferenceCallPeer : conferenceCallPeers)
- users.addChildExtension(getUser(conferenceCallPeer));
-
- iq.addExtension(users);
return iq;
}
@@ -486,8 +390,8 @@ public class OperationSetTelephonyConferencingJabberImpl
if (callPeer != null)
{
if (logger.isDebugEnabled())
- logger.debug("Processing COIN from" + coinIQ.getFrom()
- + "(version=" + coinIQ.getVersion() + ")");
+ logger.debug("Processing COIN from " + coinIQ.getFrom()
+ + " (version=" + coinIQ.getVersion() + ")");
handleCoin(callPeer, coinIQ);
}
}
@@ -504,6 +408,53 @@ public class OperationSetTelephonyConferencingJabberImpl
*/
private void handleCoin(CallPeerJabberImpl callPeer, CoinIQ coinIQ)
{
- setConferenceInfoXML(callPeer, -1, coinIQ.getChildElementXML());
+ try
+ {
+ setConferenceInfoXML(callPeer, coinIQ.getChildElementXML());
+ }
+ catch (XMLException e)
+ {
+ logger.error("Could not handle received COIN from " + callPeer
+ + ": " + coinIQ);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * For COINs (XEP-0298), we use the attributes of the
+ * <tt>conference-info</tt> element to piggyback a Jingle SID. This is
+ * temporary and should be removed once we choose a better way to pass the
+ * SID.
+ */
+ protected ConferenceInfoDocument getCurrentConferenceInfo(
+ MediaAwareCallPeer<?,?,?> callPeer)
+ {
+ ConferenceInfoDocument confInfo
+ = super.getCurrentConferenceInfo(callPeer);
+
+ if (callPeer instanceof CallPeerJabberImpl)
+ {
+ confInfo.setSid(((CallPeerJabberImpl)callPeer).getSID());
+ }
+ return confInfo;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String getLocalEntity(CallPeer callPeer)
+ {
+ return "xmpp:" + parentProvider.getOurJID();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String getLocalDisplayName()
+ {
+ return null;
}
}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/jabber.provider.manifest.mf b/src/net/java/sip/communicator/impl/protocol/jabber/jabber.provider.manifest.mf
index 1d618cb..c4a1696 100644
--- a/src/net/java/sip/communicator/impl/protocol/jabber/jabber.provider.manifest.mf
+++ b/src/net/java/sip/communicator/impl/protocol/jabber/jabber.provider.manifest.mf
@@ -44,6 +44,7 @@ Import-Package: ch.imvs.sdes4j.srtp,
org.jitsi.service.resources,
org.jitsi.service.version,
org.jitsi.util,
+ org.jitsi.util.xml,
org.jivesoftware.smack,
org.jivesoftware.smack.filter,
org.jivesoftware.smack.packet,
diff --git a/src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java
index b0963b7..874225f 100644
--- a/src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java
+++ b/src/net/java/sip/communicator/impl/protocol/sip/CallPeerSipImpl.java
@@ -1681,4 +1681,14 @@ public class CallPeerSipImpl
else
setState(CallPeerState.DISCONNECTED, reason);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getEntity()
+ {
+ return AbstractOperationSetTelephonyConferencing
+ .stripParametersFromAddress(getURI());
+ }
+
}
diff --git a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetTelephonyConferencingSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetTelephonyConferencingSipImpl.java
index 596b92b..8202f19 100644
--- a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetTelephonyConferencingSipImpl.java
+++ b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetTelephonyConferencingSipImpl.java
@@ -21,15 +21,13 @@ import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.media.*;
import net.java.sip.communicator.util.*;
-
-import org.jitsi.service.neomedia.*;
-import org.jitsi.service.neomedia.MediaType;
import org.jitsi.util.xml.*;
/**
* Implements <tt>OperationSetTelephonyConferencing</tt> for SIP.
*
* @author Lyubomir Marinov
+ * @author Boris Grozev
*/
public class OperationSetTelephonyConferencingSipImpl
extends AbstractOperationSetTelephonyConferencing<
@@ -40,7 +38,6 @@ public class OperationSetTelephonyConferencingSipImpl
Address>
implements MethodProcessorListener
{
-
/**
* The <tt>Logger</tt> used by the
* <tt>OperationSetTelephonyConferencingSipImpl</tt> class and its instances
@@ -56,28 +53,6 @@ public class OperationSetTelephonyConferencingSipImpl
private static final String CONTENT_SUB_TYPE = "conference-info+xml";
/**
- * The name of the conference-info XML element
- * <tt>conference-description</tt>.
- */
- private static final String ELEMENT_CONFERENCE_DESCRIPTION
- = "conference-description";
-
- /**
- * The name of the conference-info XML element <tt>conference-info</tt>.
- */
- private static final String ELEMENT_CONFERENCE_INFO = "conference-info";
-
- /**
- * The name of the conference-info XML element <tt>conference-state</tt>.
- */
- private static final String ELEMENT_CONFERENCE_STATE = "conference-state";
-
- /**
- * The name of the conference-info XML element <tt>user-count</tt>.
- */
- private static final String ELEMENT_USER_COUNT = "user-count";
-
- /**
* The name of the event package supported by
* <tt>OperationSetTelephonyConferencingSipImpl</tt> in SUBSCRIBE and NOTIFY
* requests.
@@ -99,12 +74,6 @@ public class OperationSetTelephonyConferencingSipImpl
private static final int SUBSCRIPTION_DURATION = 3600;
/**
- * The utility which encodes text so that it's acceptable as the text of an
- * XML element or attribute.
- */
- private DOMElementWriter domElementWriter = new DOMElementWriter();
-
- /**
* The <tt>EventPackageNotifier</tt> which implements conference
* event-package notifier support on behalf of this
* <tt>OperationSetTelephonyConferencing</tt> instance.
@@ -283,267 +252,44 @@ public class OperationSetTelephonyConferencingSipImpl
/**
* Generates the conference-info XML to be sent to a specific
* <tt>CallPeer</tt> in order to notify it of the current state of the
- * conference managed by the local peer.
+ * conference managed by the local peer. Return <tt>null</tt> if
+ * conference-info XML does not need to be sent to <tt>callPeer</tt>.
*
* @param callPeer the <tt>CallPeer</tt> to generate conference-info XML for
- * @param version the value of the version attribute of the
* <tt>conference-info</tt> root element of the conference-info XML to be
* generated
* @return the conference-info XML to be sent to the specified
* <tt>callPeer</tt> in order to notify it of the current state of the
- * conference managed by the local peer
- */
- private String getConferenceInfoXML(CallPeerSipImpl callPeer, int version)
- {
- Dialog dialog = callPeer.getDialog();
- String localParty = null;
-
- if (dialog != null)
- {
- Address localPartyAddress = dialog.getLocalParty();
-
- if (localPartyAddress != null)
- localParty
- = stripParametersFromAddress(
- localPartyAddress.getURI().toString());
- }
-
- StringBuffer xml = new StringBuffer();
-
- xml.append( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
- // <conference-info>
- append(xml, "<", ELEMENT_CONFERENCE_INFO);
- // entity
- append(xml, " entity=\"", domElementWriter.encode(localParty), "\"");
- // state
- xml.append(" state=\"full\"");
- // version
- append(xml, " version=\"", Integer.toString(version), "\">");
- // <conference-description/>
- append(xml, "<", ELEMENT_CONFERENCE_DESCRIPTION, "/>");
- // <conference-state>
- append(xml, "<", ELEMENT_CONFERENCE_STATE, ">");
- // <user-count>
- append(xml, "<", ELEMENT_USER_COUNT, ">");
-
- CallSipImpl call = callPeer.getCall();
- List<CallPeer> conferenceCallPeers = CallConference.getCallPeers(call);
-
- xml.append(1 /* the local peer/user */ + conferenceCallPeers.size());
- // </user-count>
- append(xml, "</", ELEMENT_USER_COUNT, ">");
- // </conference-state>
- append(xml, "</", ELEMENT_CONFERENCE_STATE, ">");
- // <users>
- append(xml, "<", ELEMENT_USERS, ">");
-
- // <user>
- append(xml, "<", ELEMENT_USER);
- // entity
- append(xml, " entity=\"", domElementWriter.encode(localParty), "\"");
- // state
- xml.append(" state=\"full\">");
-
- String ourDisplayName = parentProvider.getOurDisplayName();
-
- if (ourDisplayName != null)
- {
- // <display-text>
- append(xml, "<", ELEMENT_DISPLAY_TEXT, ">");
- xml.append(domElementWriter.encode(ourDisplayName));
- // </display-text>
- append(xml, "</", ELEMENT_DISPLAY_TEXT, ">");
- }
- // <endpoint>
- append(xml, "<", ELEMENT_ENDPOINT, ">");
- // <status>
- append(xml, "<", ELEMENT_STATUS, ">");
- // We are the conference focus so we're connected to the conference.
- xml.append(AbstractConferenceMember.CONNECTED);
- // </status>
- append(xml, "</", ELEMENT_STATUS, ">");
- getMediaXML(callPeer, false, xml);
- // </endpoint>
- append(xml, "</", ELEMENT_ENDPOINT, ">");
- // </user>
- append(xml, "</", ELEMENT_USER, ">");
-
- for (CallPeer conferenceCallPeer : conferenceCallPeers)
- getUserXML(conferenceCallPeer, xml);
-
- // </users>
- append(xml, "</", ELEMENT_USERS, ">");
- // </conference-info>
- append(xml, "</", ELEMENT_CONFERENCE_INFO, ">");
- return xml.toString();
- }
-
- /**
- * Generates the text content to be put in the <tt>status</tt> XML element
- * of an <tt>endpoint</tt> XML element and which describes the state of a
- * specific <tt>CallPeer</tt>.
- *
- * @param callPeer the <tt>CallPeer</tt> which is to get its state described
- * in a <tt>status</tt> XML element of an <tt>endpoint</tt> XML element
- * @return the text content to be put in the <tt>status</tt> XML element of
- * an <tt>endpoint</tt> XML element and which describes the state of the
- * specified <tt>callPeer</tt>
- */
- private String getEndpointStatusXML(CallPeer callPeer)
- {
- CallPeerState callPeerState = callPeer.getState();
-
- if (CallPeerState.ALERTING_REMOTE_SIDE.equals(callPeerState))
- return AbstractConferenceMember.ALERTING;
- if (CallPeerState.CONNECTING.equals(callPeerState)
- || CallPeerState
- .CONNECTING_WITH_EARLY_MEDIA.equals(callPeerState))
- return AbstractConferenceMember.PENDING;
- if (CallPeerState.DISCONNECTED.equals(callPeerState))
- return AbstractConferenceMember.DISCONNECTED;
- if (CallPeerState.INCOMING_CALL.equals(callPeerState))
- return AbstractConferenceMember.DIALING_IN;
- if (CallPeerState.INITIATING_CALL.equals(callPeerState))
- return AbstractConferenceMember.DIALING_OUT;
-
- /*
- * he/she is neither "hearing" the conference mix nor is his/her media
- * being mixed in the conference
- */
- if (CallPeerState.ON_HOLD_LOCALLY.equals(callPeerState)
- || CallPeerState.ON_HOLD_MUTUALLY.equals(callPeerState))
- return AbstractConferenceMember.ON_HOLD;
- if (CallPeerState.CONNECTED.equals(callPeerState))
- return AbstractConferenceMember.CONNECTED;
- return null;
- }
-
- /**
- * Appends to a specific <tt>StringBuffer</tt> <tt>media</tt> XML element
- * trees which describe the state of the media streaming between a specific
- * <tt>CallPeer</tt> and its local peer represented by an associated
- * <tt>Call</tt>.
- *
- * @param callPeer the <tt>CallPeer</tt> which is to get its media streaming
- * state described in <tt>media</tt> XML element trees appended to the
- * specified <tt>StringBuffer</tt>
- * @param remote <tt>true</tt> if the streaming from the <tt>callPeer</tt>
- * to the local peer is to be described or <tt>false</tt> if the streaming
- * from the local peer to the remote <tt>callPeer</tt> is to be described
- * @param xml the <tt>StringBuffer</tt> to append the <tt>media</tt> XML
- * trees describing the media streaming state of the specified
- * <tt>callPeer</tt>
- */
- private void getMediaXML(
- MediaAwareCallPeer<?,?,?> callPeer,
- boolean remote,
- StringBuffer xml)
- {
- CallPeerMediaHandler<?> mediaHandler = callPeer.getMediaHandler();
-
- for (MediaType mediaType : MediaType.values())
- {
- MediaStream stream = mediaHandler.getStream(mediaType);
-
- if (stream != null)
- {
- // <media>
- append(xml, "<", ELEMENT_MEDIA, ">");
- // <type>
- append(xml, "<", ELEMENT_TYPE, ">");
- xml.append(mediaType.toString());
- // </type>
- append(xml, "</", ELEMENT_TYPE, ">");
-
- long srcId
- = remote
- ? getRemoteSourceID(callPeer, mediaType)
- : stream.getLocalSourceID();
-
- if (srcId != -1)
- {
- // <src-id>
- append(xml, "<", ELEMENT_SRC_ID, ">");
- xml.append(srcId);
- // </src-id>
- append(xml, "</", ELEMENT_SRC_ID, ">");
- }
-
- MediaDirection direction
- = remote
- ? getRemoteDirection(callPeer, mediaType)
- : stream.getDirection();
-
- if (direction == null)
- direction = MediaDirection.INACTIVE;
-
- // <status>
- append(xml, "<", ELEMENT_STATUS, ">");
- xml.append(direction.toString());
- // </status>
- append(xml, "</", ELEMENT_STATUS, ">");
- // </media>
- append(xml, "</", ELEMENT_MEDIA, ">");
- }
- }
- }
-
- /**
- * Appends to a specific <tt>StringBuffer</tt> a <tt>user</tt> XML element
- * tree which describes the participation of a specific <tt>CallPeer</tt> in
- * a conference managed by the local peer represented by its associated
- * <tt>Call</tt>.
- *
- * @param callPeer the <tt>CallPeer</tt> which is to get its conference
- * participation describes in a <tt>user</tt> XML element tree appended to
- * the specified <tt>StringBuffer</tt>
- * @param xml the <tt>StringBuffer</tt> to append the <tt>user</tt> XML
- * tree describing the conference participation of the specified
- * <tt>callPeer</tt> to
+ * conference managed by the local peer. Return <tt>null</tt> if
+ * conference-info XML does not need to be sent to <tt>callPeer</tt>.
*/
- private void getUserXML(CallPeer callPeer, StringBuffer xml)
+ private String getConferenceInfoXML(CallPeerSipImpl callPeer)
{
- // <user>
- append(xml, "<", ELEMENT_USER);
- // entity
- append(
- xml,
- " entity=\"",
- domElementWriter.encode(
- stripParametersFromAddress(callPeer.getURI())),
- "\"");
- // state
- xml.append(" state=\"full\">");
-
- String displayName = callPeer.getDisplayName();
-
- if (displayName != null)
- {
- // <display-text>
- append(xml, "<", ELEMENT_DISPLAY_TEXT, ">");
- xml.append(domElementWriter.encode(displayName));
- // </display-text>
- append(xml, "</", ELEMENT_DISPLAY_TEXT, ">");
- }
- // <endpoint>
- append(xml, "<", ELEMENT_ENDPOINT, ">");
-
- String status = getEndpointStatusXML(callPeer);
-
- if (status != null)
+ ConferenceInfoDocument currentConfInfo
+ = getCurrentConferenceInfo(callPeer);
+ ConferenceInfoDocument lastSentConfInfo
+ = callPeer.getLastConferenceInfoSent();
+ ConferenceInfoDocument diff
+ = getConferenceInfoDiff(lastSentConfInfo, currentConfInfo);
+
+ if (diff == null)
+ return null;
+ else
{
- // <status>
- append(xml, "<", ELEMENT_STATUS, ">");
- xml.append(status);
- // </status>
- append(xml, "</", ELEMENT_STATUS, ">");
+ int newVersion
+ = lastSentConfInfo == null
+ ? 1
+ : lastSentConfInfo.getVersion() + 1;
+ diff.setVersion(newVersion);
+ currentConfInfo.setVersion(newVersion);
+
+ // We save currentConfInfo, because it is of state "full", while
+ // diff could be a partial
+ callPeer.setLastConferenceInfoSent(currentConfInfo);
+ callPeer.setLastConferenceInfoSentTimestamp(
+ System.currentTimeMillis());
+ return diff.toString();
}
- if (callPeer instanceof MediaAwareCallPeer<?,?,?>)
- getMediaXML((MediaAwareCallPeer<?,?,?>) callPeer, true, xml);
- // </endpoint>
- append(xml, "</", ELEMENT_ENDPOINT, ">");
- // </user>
- append(xml, "</", ELEMENT_USER, ">");
}
/**
@@ -552,7 +298,7 @@ public class OperationSetTelephonyConferencingSipImpl
* Implements the protocol-dependent part of the logic of inviting a callee
* to a <tt>Call</tt>. The protocol-independent part of that logic is
* implemented by
- * {@link AbstractOperationSetTelephonyConferencing#inviteCalleToCall(String,Call)}.
+ * {@link AbstractOperationSetTelephonyConferencing#inviteCalleeToCall(String,Call)}.
*/
@Override
protected CallPeerSipImpl doInviteCalleeToCall(
@@ -779,6 +525,37 @@ public class OperationSetTelephonyConferencingSipImpl
}
/**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String getLocalEntity(CallPeer callPeer)
+ {
+ if (callPeer instanceof CallPeerSipImpl)
+ {
+ Dialog dialog = ((CallPeerSipImpl)callPeer).getDialog();
+
+ if (dialog != null)
+ {
+ Address localPartyAddress = dialog.getLocalParty();
+
+ if (localPartyAddress != null)
+ return stripParametersFromAddress(
+ localPartyAddress.getURI().toString());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String getLocalDisplayName()
+ {
+ return parentProvider.getOurDisplayName();
+ }
+
+ /**
* Implements <tt>EventPackageNotifier.Subscription</tt> in order to
* represent a conference subscription created by a remote <tt>CallPeer</tt>
* to the conference event package of a local <tt>Call</tt>.
@@ -786,13 +563,6 @@ public class OperationSetTelephonyConferencingSipImpl
private class ConferenceNotifierSubscription
extends EventPackageNotifier.Subscription
{
-
- /**
- * The value of the <tt>version</tt> attribute to be specified in the
- * outgoing <tt>conference-info</tt> root XML elements.
- */
- private int version = 1;
-
/**
* Initializes a new <tt>ConferenceNotifierSubscription</tt> instance
* with a specific subscription <tt>Address</tt>/Request URI and a
@@ -845,29 +615,52 @@ public class OperationSetTelephonyConferencingSipImpl
return null;
}
- String conferenceInfoXML = getConferenceInfoXML(callPeer, version);
- byte[] notifyContent;
+ ConferenceInfoDocument currentConfInfo
+ = getCurrentConferenceInfo(callPeer);
+ ConferenceInfoDocument lastSentConfInfo
+ = callPeer.getLastConferenceInfoSent();
- if (conferenceInfoXML == null)
- notifyContent = null;
+ //Uncomment this when the rest of the code can handle a return value
+ //of null in case no NOTIFY needs to be sent.
+ /*
+ ConferenceInfoDocument diff
+ = lastSentConfInfo == null
+ ? currentConfInfo
+ :getConferenceInfoDiff(lastSentConfInfo, currentConfInfo);
+ */
+ ConferenceInfoDocument diff = currentConfInfo;
+
+ if (diff == null)
+ return null;
else
{
+ int newVersion
+ = lastSentConfInfo == null
+ ? 1
+ : lastSentConfInfo.getVersion() + 1;
+ diff.setVersion(newVersion);
+ currentConfInfo.setVersion(newVersion);
+
+ // We save currentConfInfo, because it is of state "full", while
+ // diff could be a partial
+ callPeer.setLastConferenceInfoSent(currentConfInfo);
+ callPeer.setLastConferenceInfoSentTimestamp(
+ System.currentTimeMillis());
+
+ String xml = diff.toXml();
+ byte[] notifyContent;
try
{
- notifyContent = conferenceInfoXML.getBytes("UTF-8");
+ notifyContent = xml.getBytes("UTF-8");
}
catch (UnsupportedEncodingException uee)
{
- logger
- .warn(
- "Failed to gets bytes from String for the UTF-8 " +
- "charset",
- uee);
- notifyContent = conferenceInfoXML.getBytes();
+ logger.warn("Failed to gets bytes from String for the "
+ + "UTF-8 charset", uee);
+ notifyContent = xml.getBytes();
}
- ++ version;
+ return notifyContent;
}
- return notifyContent;
}
/**
@@ -998,14 +791,17 @@ public class OperationSetTelephonyConferencingSipImpl
{
if (rawContent != null)
{
- int contentVersion
- = setConferenceInfoXML(
+ try
+ {
+ setConferenceInfoXML(
callPeer,
- version,
SdpUtils.getContentAsString(requestEvent.getRequest()));
-
- if (contentVersion >= version)
- version = contentVersion;
+ }
+ catch (XMLException e)
+ {
+ logger.error("Could not handle conference-info NOTIFY sent"
+ + " to us by " + callPeer);
+ }
}
}
diff --git a/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java b/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java
index 37be06b..e9a10ad 100644
--- a/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java
+++ b/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java
@@ -7,18 +7,15 @@
package net.java.sip.communicator.service.protocol.media;
import java.beans.*;
-import java.io.*;
import java.util.*;
-import javax.xml.parsers.*;
-
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.neomedia.*;
+import org.jitsi.util.xml.*;
import org.w3c.dom.*;
-import org.xml.sax.*;
/**
* Represents a default implementation of
@@ -33,6 +30,7 @@ import org.xml.sax.*;
* @param <CalleeAddressT>
*
* @author Lyubomir Marinov
+ * @author Boris Grozev
*/
public abstract class AbstractOperationSetTelephonyConferencing<
ProtocolProviderServiceT extends ProtocolProviderService,
@@ -748,21 +746,21 @@ public abstract class AbstractOperationSetTelephonyConferencing<
/**
* Updates the conference-related properties of a specific <tt>CallPeer</tt>
* such as <tt>conferenceFocus</tt> and <tt>conferenceMembers</tt> with
- * information received from it as a conference focus in the form of a
- * conference-info XML document.
+ * the information described in <tt>confInfo</tt>.
+ * <tt>confInfo</tt> must be a document with "full" state.
*
* @param callPeer the <tt>CallPeer</tt> which is a conference focus and has
* sent the specified conference-info XML document
- * @param conferenceInfoDocument the conference-info XML document sent by
- * <tt>callPeer</tt> in order to update the conference-related information
- * of the local peer represented by the associated <tt>Call</tt>
+ * @param confInfo the conference-info XML document to use to update
+ * the conference-related information of the local peer represented
+ * by the associated <tt>Call</tt>. It must have a "full" state.
*/
- private void setConferenceInfoDocument(
+ private int setConferenceInfoDocument(
MediaAwareCallPeerT callPeer,
- Document conferenceInfoDocument)
+ ConferenceInfoDocument confInfo)
{
NodeList usersList
- = conferenceInfoDocument.getElementsByTagName(ELEMENT_USERS);
+ = confInfo.getDocument().getElementsByTagName(ELEMENT_USERS);
ConferenceMember[] toRemove
= callPeer.getConferenceMembers().toArray(
AbstractCallPeer.NO_CONFERENCE_MEMBERS);
@@ -888,6 +886,9 @@ public abstract class AbstractOperationSetTelephonyConferencing<
if (changed)
notifyAll(callPeer.getCall());
+
+ callPeer.setLastConferenceInfoReceived(confInfo);
+ return confInfo.getVersion();
}
/**
@@ -898,9 +899,6 @@ public abstract class AbstractOperationSetTelephonyConferencing<
*
* @param callPeer the <tt>CallPeer</tt> which is a conference focus and has
* sent the specified conference-info XML document
- * @param version the value of the <tt>version</tt> attribute of the
- * <tt>conference-info</tt> XML element currently represented in the
- * specified <tt>callPeer</tt>
* @param conferenceInfoXML the conference-info XML document sent by
* <tt>callPeer</tt> in order to update the conference-related information
* of the local peer represented by the associated <tt>Call</tt>
@@ -908,89 +906,566 @@ public abstract class AbstractOperationSetTelephonyConferencing<
* <tt>conference-info</tt> XML element of the specified
* <tt>conferenceInfoXML</tt> if it was successfully parsed and represented
* in the specified <tt>callPeer</tt>
+ *
+ * @throws XMLException If <tt>conferenceInfoXML</tt> couldn't be parsed as
+ * a <tt>ConferenceInfoDocument</tt>
*/
protected int setConferenceInfoXML(
MediaAwareCallPeerT callPeer,
- int version,
String conferenceInfoXML)
+ throws XMLException
{
- byte[] bytes;
+ ConferenceInfoDocument confInfo
+ = new ConferenceInfoDocument(conferenceInfoXML);
- try
+ /*
+ * The CallPeer sent conference-info XML so we're sure it's a
+ * conference focus.
+ */
+ callPeer.setConferenceFocus(true);
+
+ /*
+ * The following implements the procedure outlined in section 4.6 of
+ * RFC4575 - Constructing Coherent State
+ */
+ int documentVersion = confInfo.getVersion();
+ int ourVersion = callPeer.getLastConferenceInfoReceivedVersion();
+ ConferenceInfoDocument.State documentState = confInfo.getState();
+
+ if (ourVersion == -1)
{
- bytes = conferenceInfoXML.getBytes("UTF-8");
+ if (documentState == ConferenceInfoDocument.State.FULL)
+ {
+ return setConferenceInfoDocument(callPeer, confInfo);
+ }
+ else
+ {
+ logger.warn("Received a conference-info document with state '"
+ + documentState + "'. Cannot apply it, because we haven't"
+ + "initialized a local document yet. Sending peer: "
+ + callPeer);
+ return -1;
+ }
}
- catch (UnsupportedEncodingException uee)
+ else if (documentVersion <= ourVersion)
{
- logger
- .warn(
- "Failed to gets bytes from String for the UTF-8 charset",
- uee);
- bytes = conferenceInfoXML.getBytes();
+ if (logger.isInfoEnabled())
+ {
+ logger.info("Received a stale conference-info document. Local "
+ + "version " + ourVersion + ", document version "
+ + documentVersion + ". Sending peer: " + callPeer);
+ }
+ return -1;
}
+ else //ourVersion != -1 && ourVersion < documentVersion
+ {
+ if (documentState == ConferenceInfoDocument.State.FULL)
+ return setConferenceInfoDocument(callPeer, confInfo);
+ else if (documentState == ConferenceInfoDocument.State.DELETED)
+ {
+ logger.warn("Received a conference-info document with state" +
+ "'deleted', can't handle. Sending peer: " + callPeer);
+ return -1;
+ }
+ else if (documentState == ConferenceInfoDocument.State.PARTIAL)
+ {
+ if (documentVersion == ourVersion+1)
+ return updateConferenceInfoDocument(callPeer, confInfo);
+ else
+ {
+ /*
+ * According to RFC4575 we "MUST generate a subscription
+ * refresh request to trigger a full state notification".
+ */
+ logger.warn("Received a Conference Information document "
+ + "with state '" + documentState + "' and version "
+ + documentVersion + ". Cannon apply it, because local "
+ + "version is " + ourVersion + ". Sending peer: "
+ + callPeer);
+ return -1;
+ }
+ }
+ else
+ return -1; //unreachable
+ }
+ }
+
+ /**
+ * Removes the parameters (specified after a semicolon) from a specific
+ * address <tt>String</tt> if any are present in it.
+ *
+ * @param address the <tt>String</tt> value representing an address from
+ * which any parameters are to be removed
+ * @return a <tt>String</tt> representing the specified <tt>address</tt>
+ * without any parameters
+ */
+ public static String stripParametersFromAddress(String address)
+ {
+ if (address != null)
+ {
+ int parametersBeginIndex = address.indexOf(';');
- Document doc = null;
- Throwable exception = null;
+ if (parametersBeginIndex > -1)
+ address = address.substring(0, parametersBeginIndex);
+ }
+ return address;
+ }
+ /**
+ * Creates a <tt>ConferenceInfoDocument</tt> which describes the current
+ * state of the conference in which <tt>callPeer</tt> participates. The
+ * created document contains a "full" description (as opposed to a partial
+ * description, see RFC4575).
+ *
+ * @return a <tt>ConferenceInfoDocument</tt> which describes the current
+ * state of the conference in which this <tt>CallPeer</tt> participates.
+ */
+ protected ConferenceInfoDocument getCurrentConferenceInfo(
+ MediaAwareCallPeer<?,?,?> callPeer)
+ {
+ ConferenceInfoDocument confInfo;
try
{
- doc
- = DocumentBuilderFactory.newInstance().newDocumentBuilder()
- .parse(new ByteArrayInputStream(bytes));
+ confInfo = new ConferenceInfoDocument();
}
- catch (IOException ioe)
+ catch (XMLException e)
{
- exception = ioe;
+ return null;
}
- catch (ParserConfigurationException pce)
+ confInfo.setState(ConferenceInfoDocument.State.FULL);
+ confInfo.setEntity(getLocalEntity(callPeer));
+
+ Call call = callPeer.getCall();
+ List<CallPeer> conferenceCallPeers = CallConference.getCallPeers(call);
+ confInfo.setUserCount(
+ 1 /* the local peer/user */ + conferenceCallPeers.size());
+
+ /* The local user */
+ addPeerToConferenceInfo(confInfo, callPeer, false);
+
+ /* Remote users */
+ for (CallPeer conferenceCallPeer : conferenceCallPeers)
{
- exception = pce;
+ if (conferenceCallPeer instanceof MediaAwareCallPeer<?,?,?>)
+ addPeerToConferenceInfo(
+ confInfo,
+ (MediaAwareCallPeer<?,?,?>)conferenceCallPeer,
+ true);
}
- catch (SAXException saxe)
+
+ return confInfo;
+ }
+
+ /**
+ * Adds a <tt>user</tt> element to <tt>confInfo</tt> which describes
+ * <tt>callPeer</tt>, or the local peer if <tt>remote</tt> is <tt>false</tt>.
+ *
+ * @param confInfo the <tt>ConferenceInformationDocument</tt> to which to
+ * add a <tt>user</tt> element
+ * @param callPeer the <tt>CallPeer</tt> which should be described
+ * @param remote <tt>true</tt> to describe <tt>callPeer</tt>, or
+ * <tt>false</tt> to describe the local peer.
+ */
+ private void addPeerToConferenceInfo(
+ ConferenceInfoDocument confInfo,
+ MediaAwareCallPeer<?,?,?> callPeer,
+ boolean remote)
+ {
+ String entity
+ = remote
+ ? callPeer.getEntity()
+ : getLocalEntity(callPeer);
+ ConferenceInfoDocument.User user = confInfo.addNewUser(entity);
+
+ String displayName
+ = remote
+ ? callPeer.getDisplayName()
+ : getLocalDisplayName();
+ user.setDisplayText(displayName);
+
+ ConferenceInfoDocument.Endpoint endpoint
+ = user.addNewEndpoint(entity);
+
+ endpoint.setStatus(
+ remote
+ ? getEndpointStatus(callPeer)
+ : ConferenceInfoDocument.EndpointStatusType.connected);
+
+ CallPeerMediaHandler<?> mediaHandler
+ = callPeer.getMediaHandler();
+
+ for (MediaType mediaType : MediaType.values())
{
- exception = saxe;
+ MediaStream stream = mediaHandler.getStream(mediaType);
+ if (stream != null)
+ {
+ ConferenceInfoDocument.Media media
+ = endpoint.addNewMedia(mediaType.toString());
+ long srcId
+ = remote
+ ? getRemoteSourceID(callPeer, mediaType)
+ : stream.getLocalSourceID();
+
+ if (srcId != -1)
+ media.setSrcId(Long.toString(srcId));
+
+ media.setType(mediaType.toString());
+
+ MediaDirection direction
+ = remote
+ ? getRemoteDirection(callPeer, mediaType)
+ : stream.getDirection();
+
+ if (direction == null)
+ direction = MediaDirection.INACTIVE;
+
+ media.setStatus(direction.toString());
+ }
}
- if (exception != null)
- logger.error("Failed to parse conference-info XML", exception);
- else
+ }
+
+ /**
+ * Returns a string to be used for the <tt>entity</tt> attribute of the
+ * <tt>user</tt> element for the local peer, in a Conference Information
+ * document to be sent to <tt>callPeer</tt>
+ *
+ * @param callPeer The <tt>CallPeer</tt> for which we are creating a
+ * Conference Information document.
+ * @return a string to be used for the <tt>entity</tt> attribute of the
+ * <tt>user</tt> element for the local peer, in a Conference Information
+ * document to be sent to <tt>callPeer</tt>
+ */
+ protected abstract String getLocalEntity(CallPeer callPeer);
+
+ /**
+ * Returns the display name for the local peer, which is to be used when
+ * we send Conference Information.
+ * @return the display name for the local peer, which is to be used when
+ * we send Conference Information.
+ */
+ protected abstract String getLocalDisplayName();
+
+ /**
+ * Gets the <tt>EndpointStatusType</tt> to use when describing
+ * <tt>callPeer</tt> in a Conference Information document.
+ *
+ * @param callPeer the <tt>CallPeer</tt> which is to get its state described
+ * in a <tt>status</tt> XML element of an <tt>endpoint</tt> XML element
+ * @return the <tt>EndpointStatusType</tt> to use when describing
+ * <tt>callPeer</tt> in a Conference Information document.
+ */
+ private ConferenceInfoDocument.EndpointStatusType getEndpointStatus(
+ CallPeer callPeer)
+ {
+ CallPeerState callPeerState = callPeer.getState();
+
+ if (CallPeerState.ALERTING_REMOTE_SIDE.equals(callPeerState))
+ return ConferenceInfoDocument.EndpointStatusType.alerting;
+ if (CallPeerState.CONNECTING.equals(callPeerState)
+ || CallPeerState
+ .CONNECTING_WITH_EARLY_MEDIA.equals(callPeerState))
+ return ConferenceInfoDocument.EndpointStatusType.pending;
+ if (CallPeerState.DISCONNECTED.equals(callPeerState))
+ return ConferenceInfoDocument.EndpointStatusType.disconnected;
+ if (CallPeerState.INCOMING_CALL.equals(callPeerState))
+ return ConferenceInfoDocument.EndpointStatusType.dialing_in;
+ if (CallPeerState.INITIATING_CALL.equals(callPeerState))
+ return ConferenceInfoDocument.EndpointStatusType.dialing_out;
+
+ /*
+ * he/she is neither "hearing" the conference mix nor is his/her
+ * media being mixed in the conference
+ */
+ if (CallPeerState.ON_HOLD_LOCALLY.equals(callPeerState)
+ || CallPeerState.ON_HOLD_MUTUALLY.equals(callPeerState))
+ return ConferenceInfoDocument.EndpointStatusType.on_hold;
+ if (CallPeerState.CONNECTED.equals(callPeerState))
+ return ConferenceInfoDocument.EndpointStatusType.connected;
+ return null;
+ }
+
+ /**
+ * @param from A document with state <tt>full</tt> from which to generate a
+ * "diff".
+ * @param to A document with state <tt>full</tt> to which to generate a
+ * "diff"
+ * @return a <tt>ConferenceInfoDocument</tt>, such that when it is applied
+ * to <tt>from</tt> using the procedure defined in section 4.6 of RFC4575,
+ * the result is <tt>to</tt>. May return <tt>null</tt> if <tt>from</tt> and
+ * <tt>to</tt> are not found to be different (that is, in case no document
+ * needs to be sent)
+ */
+ protected ConferenceInfoDocument getConferenceInfoDiff(
+ ConferenceInfoDocument from,
+ ConferenceInfoDocument to)
+ throws IllegalArgumentException
+ {
+ if (from.getState() != ConferenceInfoDocument.State.FULL)
+ throw new IllegalArgumentException("The 'from' document needs to "
+ + "have state=full");
+ if (to.getState() != ConferenceInfoDocument.State.FULL)
+ throw new IllegalArgumentException("The 'to' document needs to "
+ + "have state=full");
+
+ if (conferenceInfoDocumentsMatch(from, to))
+ return null;
+
+ return to;
+ }
+
+ /**
+ * Updates the conference-related properties of a specific <tt>CallPeer</tt>
+ * such as <tt>conferenceFocus</tt> and <tt>conferenceMembers</tt> with
+ * information received from it as a conference focus in the form of a
+ * partial conference-info XML document.
+ *
+ * @param callPeer the <tt>CallPeer</tt> which is a conference focus and has
+ * sent the specified partial conference-info XML document
+ * @param diff the partial conference-info XML document sent by
+ * <tt>callPeer</tt> in order to update the conference-related information
+ * of the local peer represented by the associated <tt>Call</tt>
+ * @return the value of the <tt>version</tt> attribute of the
+ * <tt>conference-info</tt> XML element of the specified
+ * <tt>conferenceInfoXML</tt> if it was successfully parsed and represented
+ * in the specified <tt>callPeer</tt>
+ */
+ private int updateConferenceInfoDocument(
+ MediaAwareCallPeerT callPeer,
+ ConferenceInfoDocument diff)
+ {
+ logger.warn("Received a conference-info partial notification, which we"
+ + " can't handle. Sending peer: " + callPeer);
+ if (true)
+ return -1;
+
+ ConferenceInfoDocument ourDocument
+ = callPeer.getLastConferenceInfoReceived();
+ ConferenceInfoDocument newDocument;
+
+ ConferenceInfoDocument.State usersState = diff.getUsersState();
+ if (usersState == ConferenceInfoDocument.State.FULL)
{
- /*
- * The CallPeer sent conference-info XML so we're sure it's a
- * conference focus.
- */
- callPeer.setConferenceFocus(true);
+ //if users is 'full', all its children must be full
+ newDocument = diff;
+ newDocument.setState(ConferenceInfoDocument.State.FULL);
+ }
+ else if (usersState == ConferenceInfoDocument.State.DELETED)
+ {
+ try
+ {
+ newDocument = new ConferenceInfoDocument();
+ }
+ catch (XMLException e)
+ {
+ logger.warn("Could not create a new ConferenceInfoDocument", e);
+ return -1;
+ }
+
+ newDocument.setVersion(diff.getVersion());
+ newDocument.setEntity(diff.getEntity());
+ newDocument.setUserCount(diff.getUserCount());
+ }
+ else //'partial'
+ {
+ newDocument = ourDocument;
- int documentVersion
- = Integer.parseInt(
- doc.getDocumentElement().getAttribute("version"));
+ newDocument.setVersion(diff.getVersion());
+ newDocument.setEntity(diff.getEntity());
+ newDocument.setUserCount(diff.getUserCount());
- if ((version == -1) || (documentVersion >= version))
+ for (ConferenceInfoDocument.User user : diff.getUsers())
{
- setConferenceInfoDocument(callPeer, doc);
- return documentVersion;
+ ConferenceInfoDocument.State userState = user.getState();
+ if (userState == ConferenceInfoDocument.State.FULL)
+ {
+ //copy the whole thing from diff to newDocument
+ }
+ else if (userState == ConferenceInfoDocument.State.DELETED)
+ {
+ newDocument.removeUser(user.getEntity());
+ }
+ else
+ {
+ ConferenceInfoDocument.User ourUser
+ = newDocument.getUser(user.getEntity());
+ for (ConferenceInfoDocument.Endpoint endpoint
+ : user.getEndpoints())
+ {
+ ConferenceInfoDocument.State endpointState
+ = endpoint.getState();
+ if (endpointState == ConferenceInfoDocument.State.FULL)
+ {
+ //update the whole thing
+ }
+ else if (endpointState
+ == ConferenceInfoDocument.State.DELETED)
+ {
+ ourUser.removeEndpoint(endpoint.getEntity());
+ }
+ else //'partial'
+ {
+ for (ConferenceInfoDocument.Media media
+ : endpoint.getMedias())
+ {
+ //copy media with id media.getId()
+ }
+ }
+ }
+ }
}
}
+
return -1;
}
/**
- * Removes the parameters (specified after a semicolon) from a specific
- * address <tt>String</tt> if any are present in it.
+ * @param a A document with state <tt>full</tt> which to compare to
+ * <tt>b</tt>
+ * @param b A document with state <tt>full</tt> which to compare to
+ * <tt>a</tt>
+ * @return <tt>false</tt> if the two documents are found to be different,
+ * <tt>true</tt> otherwise (that is, it can return true for non-identical
+ * documents).
+ */
+ private boolean conferenceInfoDocumentsMatch(
+ ConferenceInfoDocument a,
+ ConferenceInfoDocument b)
+ {
+ if (a.getState() != ConferenceInfoDocument.State.FULL)
+ throw new IllegalArgumentException("The 'a' document needs to"
+ + "have state=full");
+ if (b.getState() != ConferenceInfoDocument.State.FULL)
+ throw new IllegalArgumentException("The 'b' document needs to"
+ + "have state=full");
+
+ if (!stringsMatch(a.getEntity(), b.getEntity()))
+ return false;
+ else if (a.getUserCount() != b.getUserCount())
+ return false;
+ else if (a.getUsers().size() != b.getUsers().size())
+ return false;
+
+ for(ConferenceInfoDocument.User aUser : a.getUsers())
+ {
+ if (!usersMatch(aUser, b.getUser(aUser.getEntity())))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether two <tt>ConferenceInfoDocument.User</tt> instances
+ * match according to the needs of our implementation. Can return
+ * <tt>true</tt> for users which are not identical.
*
- * @param address the <tt>String</tt> value representing an address from
- * which any parameters are to be removed
- * @return a <tt>String</tt> representing the specified <tt>address</tt>
- * without any parameters
+ * @param a A <tt>ConferenceInfoDocument.User</tt> to compare
+ * @param b A <tt>ConferenceInfoDocument.User</tt> to compare
+ * @return <tt>false</tt> if <tt>a</tt> and <tt>b</tt> are found to be
+ * different in a way that is significant for our needs, <tt>true</tt>
+ * otherwise.
*/
- protected static String stripParametersFromAddress(String address)
+ private boolean usersMatch(
+ ConferenceInfoDocument.User a,
+ ConferenceInfoDocument.User b)
{
- if (address != null)
+ if (a == null && b == null)
+ return true;
+ else if (a == null || b == null)
+ return false;
+ else if (!stringsMatch(a.getEntity(), b.getEntity()))
+ return false;
+ else if (!stringsMatch(a.getDisplayText(), b.getDisplayText()))
+ return false;
+ else if (a.getEndpoints().size() != b.getEndpoints().size())
+ return false;
+
+ for (ConferenceInfoDocument.Endpoint aEndpoint : a.getEndpoints())
{
- int parametersBeginIndex = address.indexOf(';');
+ if (!endpointsMatch(aEndpoint, b.getEndpoint(aEndpoint.getEntity())))
+ return false;
+ }
- if (parametersBeginIndex > -1)
- address = address.substring(0, parametersBeginIndex);
+ return true;
+ }
+
+ /**
+ * Checks whether two <tt>ConferenceInfoDocument.Endpoint</tt> instances
+ * match according to the needs of our implementation. Can return
+ * <tt>true</tt> for endpoints which are not identical.
+ *
+ * @param a A <tt>ConferenceInfoDocument.Endpoint</tt> to compare
+ * @param b A <tt>ConferenceInfoDocument.Endpoint</tt> to compare
+ * @return <tt>false</tt> if <tt>a</tt> and <tt>b</tt> are found to be
+ * different in a way that is significant for our needs, <tt>true</tt>
+ * otherwise.
+ */
+ private boolean endpointsMatch(
+ ConferenceInfoDocument.Endpoint a,
+ ConferenceInfoDocument.Endpoint b)
+ {
+ if (a == null && b == null)
+ return true;
+ else if (a == null || b == null)
+ return false;
+ else if (!stringsMatch(a.getEntity(), b.getEntity()))
+ return false;
+ else if (a.getStatus() != b.getStatus())
+ return false;
+ else if (a.getMedias().size() != b.getMedias().size())
+ return false;
+
+ for (ConferenceInfoDocument.Media aMedia : a.getMedias())
+ {
+ if (!mediasMatch(aMedia, b.getMedia(aMedia.getId())))
+ return false;
}
- return address;
+ return true;
+ }
+
+ /**
+ * Checks whether two <tt>ConferenceInfoDocument.Media</tt> instances
+ * match according to the needs of our implementation. Can return
+ * <tt>true</tt> for endpoints which are not identical.
+ *
+ * @param a A <tt>ConferenceInfoDocument.Media</tt> to compare
+ * @param b A <tt>ConferenceInfoDocument.Media</tt> to compare
+ * @return <tt>false</tt> if <tt>a</tt> and <tt>b</tt> are found to be
+ * different in a way that is significant for our needs, <tt>true</tt>
+ * otherwise.
+ */
+ private boolean mediasMatch(
+ ConferenceInfoDocument.Media a,
+ ConferenceInfoDocument.Media b)
+ {
+ if (a == null && b == null)
+ return true;
+ else if (a == null || b == null)
+ return false;
+ else if (!stringsMatch(a.getId(), b.getId()))
+ return false;
+ else if (!stringsMatch(a.getSrcId(), b.getSrcId()))
+ return false;
+ else if (!stringsMatch(a.getType(), b.getType()))
+ return false;
+ else if (!stringsMatch(a.getStatus(), b.getStatus()))
+ return false;
+
+ return true;
}
+
+ /**
+ * @param a A <tt>String</tt> to compare to <tt>b</tt>
+ * @param b A <tt>String</tt> to compare to <tt>a</tt>
+ * @return <tt>true</tt> if and only if <tt>a</tt> and <tt>b</tt> are both
+ * <tt>null</tt>, or they are equal as <tt>String</tt>s
+ */
+ private boolean stringsMatch(String a, String b)
+ {
+ if (a == null && b == null)
+ return true;
+ else if (a == null || b == null)
+ return false;
+ return a.equals(b);
+ }
+
}
diff --git a/src/net/java/sip/communicator/service/protocol/media/ConferenceInfoDocument.java b/src/net/java/sip/communicator/service/protocol/media/ConferenceInfoDocument.java
new file mode 100644
index 0000000..39bec2b
--- /dev/null
+++ b/src/net/java/sip/communicator/service/protocol/media/ConferenceInfoDocument.java
@@ -0,0 +1,1282 @@
+/*
+ * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.service.protocol.media;
+
+import net.java.sip.communicator.util.*;
+import org.jitsi.util.xml.*;
+import org.w3c.dom.*;
+
+import javax.xml.parsers.*;
+import javax.xml.transform.*;
+import javax.xml.transform.dom.*;
+import javax.xml.transform.stream.*;
+import java.io.*;
+import java.util.*;
+
+/**
+ * A class that represents a Conference Information XML document as defined in
+ * RFC4575. It wraps around a DOM <tt>Document</tt> providing convenience
+ * functions.
+ *
+ * {@link "http://tools.ietf.org/html/rfc4575"}
+ *
+ * @author Boris Grozev
+ * @author Sebastien Vincent
+ */
+public class ConferenceInfoDocument
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>ConferenceInfoDocument</tt> class
+ * and its instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(ConferenceInfoDocument.class);
+
+ /**
+ * The namespace of the conference-info element.
+ */
+ public static final String NAMESPACE
+ = "urn:ietf:params:xml:ns:conference-info";
+
+ /**
+ * The name of the "conference-info" element.
+ */
+ public static final String CONFERENCE_INFO_ELEMENT_NAME = "conference-info";
+
+ /**
+ * The name of the "conference-description" element.
+ */
+ public static final String CONFERENCE_DESCRIPTION_ELEMENT_NAME
+ = "conference-description";
+
+ /**
+ * The name of the "conference-state" element.
+ */
+ public static final String CONFERENCE_STATE_ELEMENT_NAME
+ = "conference-state";
+
+ /**
+ * The name of the "state" attribute.
+ */
+ public static final String STATE_ATTR_NAME = "state";
+
+ /**
+ * The name of the "entity" attribute.
+ */
+ public static final String ENTITY_ATTR_NAME = "entity";
+
+ /**
+ * The name of the "version" attribute.
+ */
+ public static final String VERSION_ATTR_NAME = "version";
+
+ /**
+ * The name of the "user" element.
+ */
+ public static final String USER_ELEMENT_NAME = "user";
+
+ /**
+ * The name of the "users" element.
+ */
+ public static final String USERS_ELEMENT_NAME = "users";
+
+ /**
+ * The name of the "endpoint" element.
+ */
+ public static final String ENDPOINT_ELEMENT_NAME = "endpoint";
+
+ /**
+ * The name of the "media" element.
+ */
+ public static final String MEDIA_ELEMENT_NAME = "media";
+
+ /**
+ * The name of the "id" attribute.
+ */
+ public static final String ID_ATTR_NAME = "id";
+
+ /**
+ * The name of the "status" element.
+ */
+ public static final String STATUS_ELEMENT_NAME = "status";
+
+ /**
+ * The name of the "src-id" element.
+ */
+ public static final String SRC_ID_ELEMENT_NAME = "src-id";
+
+ /**
+ * The name of the "type" element.
+ */
+ public static final String TYPE_ELEMENT_NAME = "type";
+
+ /**
+ * The name of the "user-count" element.
+ */
+ public static final String USER_COUNT_ELEMENT_NAME = "user-count";
+
+ /**
+ * The mane of the "display-text" element.
+ */
+ public static final String DISPLAY_TEXT_ELEMENT_NAME = "display-text";
+
+ /**
+ * The <tt>Document</tt> object that we wrap around.
+ */
+ private Document document;
+
+ /**
+ * The single <tt>conference-info</tt> element of <tt>document</tt>
+ */
+ private Element conferenceInfo;
+
+ /**
+ * The <tt>conference-description</tt> child element of
+ * <tt>conference-info</tt>.
+ */
+ private Element conferenceDescription;
+
+ /**
+ * The <tt>conference-state</tt> child element of <tt>conference-info</tt>.
+ */
+ private Element conferenceState;
+
+ /**
+ * The <tt>conference-state</tt> child element of <tt>conference-state</tt>.
+ */
+ private Element userCount;
+
+ /**
+ * The <tt>users</tt> child element of <tt>conference-info</tt>.
+ */
+ private Element users;
+
+ /**
+ * A list of <tt>User</tt>s representing the children of <tt>users</tt>
+ */
+ private final List<User> usersList = new LinkedList<User>();
+
+ /**
+ * Creates a new <tt>ConferenceInfoDocument</tt> instance.
+ *
+ * @throws XMLException if a document failed to be created.
+ */
+ public ConferenceInfoDocument()
+ throws XMLException
+ {
+ try
+ {
+ document = XMLUtils.createDocument();
+ }
+ catch (Exception e)
+ {
+ logger.error("Failed to create a new document.", e);
+ throw(new XMLException(e.getMessage()));
+ }
+
+
+ conferenceInfo = document
+ .createElementNS(NAMESPACE, CONFERENCE_INFO_ELEMENT_NAME);
+ document.appendChild(conferenceInfo);
+
+ setVersion(1);
+
+ conferenceDescription
+ = document.createElement(CONFERENCE_DESCRIPTION_ELEMENT_NAME);
+ conferenceInfo.appendChild(conferenceDescription);
+
+ conferenceState = document.createElement(CONFERENCE_STATE_ELEMENT_NAME);
+ conferenceInfo.appendChild(conferenceState);
+ setUserCount(0);
+
+ users = document.createElement(USERS_ELEMENT_NAME);
+ conferenceInfo.appendChild(users);
+ }
+
+ /**
+ * Creates a new <tt>ConferenceInfoDocument</tt> instance and populates it
+ * by parsing the XML in <tt>xml</tt>
+ *
+ * @param xml the XML string to parse
+ *
+ * @throws XMLException If parsing failed
+ */
+ public ConferenceInfoDocument(String xml)
+ throws XMLException
+ {
+ byte[] bytes;
+
+ try
+ {
+ bytes = xml.getBytes("UTF-8");
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ logger.warn(
+ "Failed to gets bytes from String for the UTF-8 charset",
+ uee);
+ bytes = xml.getBytes();
+ }
+
+ try
+ {
+ document
+ = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ .parse(new ByteArrayInputStream(bytes));
+ }
+ catch (Exception e)
+ {
+ throw new XMLException(e.getMessage());
+ }
+
+ conferenceInfo = document.getDocumentElement();
+ if (conferenceInfo == null)
+ {
+ throw new XMLException("Could not parse conference-info document,"
+ + " conference-info element not found");
+ }
+
+ conferenceDescription = XMLUtils
+ .findChild(conferenceInfo, CONFERENCE_DESCRIPTION_ELEMENT_NAME);
+ //conference-description is mandatory
+ if (conferenceDescription == null)
+ {
+ throw new XMLException("Could not parse conference-info document,"
+ + " conference-description element not found");
+ }
+
+ conferenceState
+ = XMLUtils.findChild(conferenceInfo, CONFERENCE_STATE_ELEMENT_NAME);
+ if (conferenceState != null)
+ userCount = XMLUtils
+ .findChild(conferenceState, USER_COUNT_ELEMENT_NAME);
+
+ users = XMLUtils.findChild(conferenceInfo, USERS_ELEMENT_NAME);
+ if (users == null)
+ {
+ throw new XMLException("Could not parse conference-info document,"
+ + " 'users' element not found");
+ }
+ NodeList usersNodeList = users.getElementsByTagName(USER_ELEMENT_NAME);
+ for(int i=0; i<usersNodeList.getLength(); i++)
+ {
+ User user = new User((Element)usersNodeList.item(i));
+ usersList.add(user);
+ }
+ }
+
+ /**
+ * Returns the value of the <tt>version</tt> attribute of the
+ * <tt>conference-info</tt> element, or -1 if there is no <tt>version</tt>
+ * attribute or if it's value couldn't be parsed as an integer.
+ * @return the value of the <tt>version</tt> attribute of the
+ * <tt>conference-info</tt> element, or -1 if there is no <tt>version</tt>
+ * attribute or if it's value couldn't be parsed as an integer.
+ */
+ public int getVersion()
+ {
+ String versionString = conferenceInfo.getAttribute(VERSION_ATTR_NAME);
+ if (versionString == null)
+ return -1;
+ int version = -1;
+ try
+ {
+ version = Integer.parseInt(versionString);
+ }
+ catch (NumberFormatException e)
+ {
+ if (logger.isInfoEnabled())
+ logger.info("Failed to parse version string: " + versionString);
+ }
+
+ return version;
+ }
+
+ /**
+ * Sets the <tt>version</tt> attribute of the <tt>conference-info</tt>
+ * element.
+ * @param version the value to set the <tt>version</tt> attribute of the
+ * <tt>conference-info</tt> element to.
+ */
+ public void setVersion(int version)
+ {
+ conferenceInfo.setAttribute(VERSION_ATTR_NAME, Integer.toString(version));
+ }
+
+ /**
+ * Gets the value of the <tt>state</tt> attribute of the
+ * <tt>conference-info</tt> element.
+ * @return the value of the <tt>state</tt> attribute of the
+ * <tt>conference-info</tt> element.
+ */
+ public State getState()
+ {
+ return getState(conferenceInfo);
+ }
+
+ /**
+ * Returns the value of the <tt>state</tt> attribute of the <tt>users</tt>
+ * child of the <tt>conference-info</tt> element.
+ *
+ * @return the value of the <tt>state</tt> attribute of the <tt>users</tt>
+ * child of the <tt>conference-info</tt> element.
+ */
+ public State getUsersState()
+ {
+ return getState(users);
+ }
+
+ /**
+ * Sets the value of the <tt>state</tt> attribute of the
+ * <tt>conference-info</tt> element.
+ * @param state the value to set the <tt>state</tt> attribute of the
+ * <tt>conference-info</tt> element to.
+ */
+ public void setState(State state)
+ {
+ setState(conferenceInfo, state);
+ }
+
+ /**
+ * Sets the value of the <tt>sid</tt> attribute of the
+ * <tt>conference-info</tt> element.
+ * This is not part of RFC4575 and is here because we are temporarily using
+ * it in our XMPP implementation.
+ * TODO: remote it when we define another way to handle the Jingle SID
+ *
+ * @param sid the value to set the <tt>sid</tt> attribute of the
+ * <tt>conference-info</tt> element to.
+ */
+ public void setSid(String sid)
+ {
+ conferenceInfo.setAttribute("sid", sid);
+ }
+
+ /**
+ * Sets the value of the <tt>entity</tt> attribute of the
+ * <tt>conference-info</tt> element.
+ * @param entity the value to set the <tt>entity</tt> attribute of the
+ * <tt>conference-info</tt> document to.
+ */
+ public void setEntity(String entity)
+ {
+ conferenceInfo.setAttribute(ENTITY_ATTR_NAME, entity);
+ }
+
+ /**
+ * Gets the value of the <tt>entity</tt> attribute of the
+ * <tt>conference-info</tt> element.
+ * @return The value of the <tt>entity</tt> attribute of the
+ * <tt>conference-info</tt> element.
+ */
+ public String getEntity()
+ {
+ return conferenceInfo.getAttribute(ENTITY_ATTR_NAME);
+ }
+
+ /**
+ * Sets the content of the <tt>user-count</tt> child element of the
+ * <tt>conference-state</tt> child element of <tt>conference-info</tt>
+ * @param count the value to set the content of <tt>user-count</tt> to
+ */
+ public void setUserCount(int count)
+ {
+ // conference-state and its user-count child aren't mandatory
+ if (userCount != null)
+ {
+ userCount.setTextContent(Integer.toString(count));
+ }
+ else
+ {
+ if (conferenceState == null)
+ {
+ conferenceState
+ = document.createElement(CONFERENCE_STATE_ELEMENT_NAME);
+ conferenceInfo.appendChild(conferenceState);
+ }
+
+ userCount = document.createElement(USER_COUNT_ELEMENT_NAME);
+ userCount.setTextContent(Integer.toString(count));
+ conferenceState.appendChild(userCount);
+ }
+ }
+
+ /**
+ * Returns the content of the <tt>user-count</tt> child of the
+ * <tt>conference-state</tt> child of <tt>conference-info</tt>, parsed as
+ * an integer, if they exist. Returns -1 if either there isn't a
+ * <tt>conference-state</tt> element, it doesn't have a <tt>user-count</tt>
+ * child, or parsing as integer failed.
+ *
+ * @return the content of the <tt>user-count</tt> child of the
+ * <tt>conference-state</tt> child of <tt>conference-info</tt> element.
+ */
+ public int getUserCount()
+ {
+ int ret = -1;
+ try
+ {
+ ret = Integer.parseInt(userCount.getTextContent());
+ }
+ catch (Exception e)
+ {
+ logger.warn("Could not parse user-count field");
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the XML representation of the <tt>conference-info</tt> tree,
+ * or <tt>null</tt> if an error occurs while trying to get it.
+ *
+ * @return the XML representation of the <tt>conference-info</tt> tree,
+ * or <tt>null</tt> if an error occurs while trying to get it.
+ */
+ public String toXml()
+ {
+ try
+ {
+ Transformer transformer
+ = TransformerFactory.newInstance().newTransformer();
+ StringWriter buffer = new StringWriter();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,
+ "yes");
+ transformer.transform(new DOMSource(conferenceInfo),
+ new StreamResult(buffer));
+ return buffer.toString();
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the XML representation of the document (from the
+ * <tt>conference-info</tt> element down), or an error string in case the
+ * XML cannot be generated for some reason.
+ * @return the XML representation of the document or an error string.
+ */
+ @Override
+ public String toString()
+ {
+ String s = toXml();
+ return s == null
+ ? "Could not get conference-info XML"
+ : s;
+ }
+
+ /**
+ * Returns the list of <tt>User</tt> that represents the <tt>user</tt>
+ * children of the <tt>users</tt> child element of <tt>conference-info</tt>
+ * @return the list of <tt>User</tt> that represents the <tt>user</tt>
+ * children of the <tt>users</tt> child element of <tt>conference-info</tt>
+ */
+ public List<User> getUsers()
+ {
+ return usersList;
+ }
+
+ /**
+ * Searches this document's <tt>User</tt>s and returns the one with
+ * <tt>entity</tt> attribute <tt>entity</tt>, or <tt>null</tt> if one
+ * wasn't found.
+ * @param entity The value of the <tt>entity</tt> attribute to search for.
+ * @return the <tt>User</tt> of this document with <tt>entity</tt>
+ * attribute <tt>entity</tt>, or <tt>null</tt> if one wasn't found.
+ * */
+ public User getUser(String entity)
+ {
+ if (entity == null)
+ return null;
+ for(User u : usersList)
+ {
+ if (entity.equals(u.getEntity()))
+ return u;
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new <tt>User</tt> instance, adds it to the document and
+ * returns it.
+ * @param entity The value to use for the <tt>entity</tt> attribute of the
+ * new <tt>User</tt>.
+ * @return the newly created <tt>User</tt> instance.
+ */
+ public User addNewUser(String entity)
+ {
+ Element userElement = document.createElement(USER_ELEMENT_NAME);
+ User user = new User(userElement);
+ user.setEntity(entity);
+
+ users.appendChild(userElement);
+ usersList.add(user);
+
+ return user;
+ }
+
+ /**
+ * Removes a specific <tt>User</tt> (the one with entity <tt>entity</tt>)
+ * from the document.
+ * @param entity the entity of the <tt>User</tt> to remove.
+ */
+ public void removeUser(String entity)
+ {
+ User user = getUser(entity);
+ if (user != null)
+ {
+ usersList.remove(user);
+ users.removeChild(user.userElement);
+ }
+ }
+
+ /**
+ * Returns the <tt>Document</tt> that this instance wraps around.
+ * @return the <tt>Document</tt> that this instance wraps around.
+ */
+ public Document getDocument()
+ {
+ return document;
+ }
+
+ /**
+ * Returns the <tt>State</tt> corresponding to the <tt>state</tt> attribute
+ * of an <tt>Element</tt>. Default to <tt>State.FULL</tt> which is the
+ * RFC4575 default.
+ * @param element the <tt>Element</tt>
+ * @return the <tt>State</tt> corresponding to the <tt>state</tt> attribute
+ * of an <tt>Element</tt>.
+ */
+ private State getState(Element element)
+ {
+ State state = State.parseString(element.getAttribute(STATE_ATTR_NAME));
+ return state == null
+ ? State.FULL
+ : state;
+ }
+
+ /**
+ * Sets the "state" attribute of <tt>element</tt> to <tt>state</tt>.
+ * If <tt>state</tt> is <tt>State.FULL</tt> removes the "state" attribute,
+ * because this is the default value.
+ * @param element The <tt>Element</tt> for which to set the "state"
+ * attribute of.
+ * @param state the <tt>State</tt> which to set.
+ */
+ private void setState(Element element, State state)
+ {
+ if (element != null)
+ {
+ if (state == State.FULL)
+ element.removeAttribute(STATE_ATTR_NAME);
+ else
+ element.setAttribute(STATE_ATTR_NAME, state.toString());
+ }
+ }
+
+ /**
+ * Sets the <tt>status</tt> child element of <tt>element</tt>. If
+ * <tt>statusString</tt> is <tt>null</tt>, the child element is removed
+ * if present.
+ * @param element the <tt>Element</tt> for which to set the <tt>status</tt>
+ * child element.
+ * @param statusString the <tt>String</tt> to use for the text content of
+ * the <tt>status</tt> element
+ */
+ private void setStatus(Element element, String statusString)
+ {
+ Element statusElement
+ = XMLUtils.findChild(element, STATUS_ELEMENT_NAME);
+ if (statusString == null)
+ {
+ if(statusElement == null)
+ return;
+ else
+ element.removeChild(statusElement);
+ }
+ else
+ {
+ if (statusElement == null)
+ {
+ statusElement = document.createElement(STATUS_ELEMENT_NAME);
+ element.appendChild(statusElement);
+ }
+ statusElement.setTextContent(statusString);
+ }
+ }
+
+ /**
+ * Represents the possible values for the <tt>state</tt> attribute (see
+ * RFC4575)
+ */
+ public enum State
+ {
+ /**
+ * State <tt>full</tt>
+ */
+ FULL("full"),
+
+ /**
+ * State <tt>partial</tt>
+ */
+ PARTIAL("partial"),
+
+ /**
+ * State <tt>deleted</tt>
+ */
+ DELETED("deleted");
+
+ /**
+ * The name of this <tt>State</tt>
+ */
+ private String name;
+
+ /**
+ * Creates a <tt>State</tt> instance with the specified name.
+ * @param name
+ */
+ private State(String name)
+ {
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of this <tt>State</tt>
+ * @return the name of this <tt>State</tt>
+ */
+ @Override
+ public String toString()
+ {
+ return name;
+ }
+
+ /**
+ * Returns a <tt>State</tt> value corresponding to the specified
+ * <tt>name</tt>
+ * @return a <tt>State</tt> value corresponding to the specified
+ * <tt>name</tt>
+ */
+ public static State parseString(String name)
+ {
+ if (FULL.toString().equals(name))
+ return FULL;
+ else if(PARTIAL.toString().equals(name))
+ return PARTIAL;
+ else if(DELETED.toString().equals(name))
+ return DELETED;
+ else
+ return null;
+ }
+ }
+
+ /**
+ * Wraps around an <tt>Element</tt> and represents a <tt>user</tt>
+ * element (child of the <tt>users</tt> element). See RFC4575.
+ */
+ public class User
+ {
+ /**
+ * The underlying <tt>Element</tt>.
+ */
+ private Element userElement;
+
+ /**
+ * The list of <tt>Endpoint</tt>s representing the <tt>endpoint</tt>
+ * children of this <tt>User</tt>'s element.
+ */
+ private List<Endpoint> endpointsList = new LinkedList<Endpoint>();
+
+ /**
+ * Creates a new <tt>User</tt> instance with the specified
+ * <tt>Element</tt> as its underlying element.
+ * @param user the <tt>Element</tt> to use
+ */
+ private User(Element user)
+ {
+ this.userElement = user;
+ NodeList endpointsNodeList
+ = user.getElementsByTagName(ENDPOINT_ELEMENT_NAME);
+ for (int i=0; i<endpointsNodeList.getLength(); i++)
+ {
+ Endpoint endpoint
+ = new Endpoint((Element)endpointsNodeList.item(i));
+ endpointsList.add(endpoint);
+ }
+ }
+
+ /**
+ * Sets the <tt>entity</tt> attribute of this <tt>User</tt>'s element
+ * to <tt>entity</tt>
+ * @param entity the value to set for the <tt>entity</tt> attribute.
+ */
+ public void setEntity(String entity)
+ {
+ userElement.setAttribute(ENTITY_ATTR_NAME, entity);
+ }
+
+ /**
+ * Returns the value of the <tt>entity</tt> attribute of this
+ * <tt>User</tt>'s element.
+ * @return the value of the <tt>entity</tt> attribute of this
+ * <tt>User</tt>'s element.
+ */
+ public String getEntity()
+ {
+ return userElement.getAttribute(ENTITY_ATTR_NAME);
+ }
+
+ /**
+ * Sets the <tt>state</tt> attribute of this <tt>User</tt>'s element to
+ * <tt>state</tt>
+ * @param state the value to use for the <tt>state</tt> attribute.
+ */
+ public void setState(State state)
+ {
+ ConferenceInfoDocument.this.setState(userElement, state);
+ }
+
+ /**
+ * Returns the value of the <tt>state</tt> attribute of this
+ * <tt>User</tt>'s element
+ * @return the value of the <tt>state</tt> attribute of this
+ * <tt>User</tt>'s element
+ */
+ public State getState()
+ {
+ return ConferenceInfoDocument.this.getState(userElement);
+ }
+
+ /**
+ * Sets the <tt>display-text</tt> child element to this <tt>User</tt>'s
+ * element.
+ * @param text the text content to use for the <tt>display-text</tt>
+ * element.
+ */
+ public void setDisplayText(String text)
+ {
+ Element displayText
+ = XMLUtils.findChild(userElement, DISPLAY_TEXT_ELEMENT_NAME);
+ if (text == null || text.equals(""))
+ {
+ if (displayText == null)
+ return;
+ else
+ userElement.removeChild(displayText);
+ }
+ else
+ {
+ if (displayText == null)
+ {
+ displayText
+ = document.createElement(DISPLAY_TEXT_ELEMENT_NAME);
+ userElement.appendChild(displayText);
+ }
+ displayText.setTextContent(text);
+ }
+ }
+
+ /**
+ * Returns the text content of the <tt>display-text</tt> child element
+ * of this <tt>User</tt>'s element, if it has such a child. Returns
+ * <tt>null</tt> otherwise.
+ * @return the text content of the <tt>display-text</tt> child element
+ * of this <tt>User</tt>'s element, if it has such a child. Returns
+ * <tt>null</tt> otherwise.
+ */
+ public String getDisplayText()
+ {
+ Element displayText
+ = XMLUtils.findChild(userElement, DISPLAY_TEXT_ELEMENT_NAME);
+ if (displayText != null)
+ return displayText.getTextContent();
+
+ return null;
+ }
+
+ /**
+ * Returns the list of <tt>Endpoint</tt>s which represent the
+ * <tt>endpoint</tt> children of this <tt>User</tt>'s element.
+ * @return the list of <tt>Endpoint</tt>s which represent the
+ * <tt>endpoint</tt> children of this <tt>User</tt>'s element.
+ */
+ public List<Endpoint> getEndpoints()
+ {
+ return endpointsList;
+ }
+
+ /**
+ * Searches this <tt>User</tt>'s associated <tt>Endpoint</tt>s
+ * and returns the one with <tt>entity</tt> attribute <tt>entity</tt>,
+ * or <tt>null</tt> if one wasn't found.
+ * @param entity The value of the <tt>entity</tt> attribute to search
+ * for.
+ * @return The <tt>Endpoint</tt> with <tt>entity</tt> attribute
+ * <tt>entity</tt>, or <tt>null</tt> if one wasn't found.
+ */
+ public Endpoint getEndpoint(String entity)
+ {
+ if (entity == null)
+ return null;
+ for (Endpoint e : endpointsList)
+ {
+ if (entity.equals(e.getEntity()))
+ return e;
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new <tt>Endpoint</tt> instance, adds it to this
+ * <tt>User</tt> and returns it.
+ * @param entity The value to use for the <tt>entity</tt> attribute of
+ * the new <tt>Endpoint</tt>.
+ * @return the newly created <tt>Endpoint</tt> instance.
+ */
+ public Endpoint addNewEndpoint(String entity)
+ {
+ Element endpointElement
+ = document.createElement(ENDPOINT_ELEMENT_NAME);
+ Endpoint endpoint = new Endpoint(endpointElement);
+ endpoint.setEntity(entity);
+
+ userElement.appendChild(endpointElement);
+ endpointsList.add(endpoint);
+
+ return endpoint;
+ }
+
+ /**
+ * Removes a specific <tt>Endpoint</tt> (the one with entity
+ * <tt>entity</tt>) from this <tt>User</tt>.
+ * @param entity the <tt>entity</tt> of the <tt>Endpoint</tt> to remove
+ */
+ public void removeEndpoint(String entity)
+ {
+ Endpoint endpoint = getEndpoint(entity);
+ if (endpoint != null)
+ {
+ endpointsList.remove(endpoint);
+ userElement.removeChild(endpoint.endpointElement);
+ }
+ }
+ }
+
+ /**
+ * Wraps around an <tt>Element</tt> and represents an <tt>endpoint</tt>
+ * element. See RFC4575.
+ */
+ public class Endpoint
+ {
+ /**
+ * The underlying <tt>Element</tt>.
+ */
+ private Element endpointElement;
+
+ /**
+ * The list of <tt>Media</tt>s representing the <tt>media</tt>
+ * children elements of this <tt>Endpoint</tt>'s element.
+ */
+ private List<Media> mediasList = new LinkedList<Media>();
+
+ /**
+ * Creates a new <tt>Endpoint</tt> instance with the specified
+ * <tt>Element</tt> as its underlying element.
+ * @param endpoint the <tt>Element</tt> to use
+ */
+ private Endpoint(Element endpoint)
+ {
+ this.endpointElement = endpoint;
+ NodeList mediaNodeList
+ = endpoint.getElementsByTagName(MEDIA_ELEMENT_NAME);
+ for (int i=0; i<mediaNodeList.getLength(); i++)
+ {
+ Media media = new Media((Element)mediaNodeList.item(i));
+ mediasList.add(media);
+ }
+ }
+
+ /**
+ * Sets the <tt>entity</tt> attribute of this <tt>Endpoint</tt>'s
+ * element to <tt>entity</tt>
+ * @param entity the value to set for the <tt>entity</tt> attribute.
+ */
+ public void setEntity(String entity)
+ {
+ endpointElement.setAttribute(ENTITY_ATTR_NAME, entity);
+ }
+
+ /**
+ * Returns the <tt>entity</tt> attribute of this <tt>Endpoint</tt>'s
+ * element.
+ * @return the <tt>entity</tt> attribute of this <tt>Endpoint</tt>'s
+ * element.
+ */
+ public String getEntity()
+ {
+ return endpointElement.getAttribute(ENTITY_ATTR_NAME);
+ }
+
+ /**
+ * Sets the <tt>state</tt> attribute of this <tt>User</tt>'s element to
+ * <tt>state</tt>
+ * @param state the value to use for the <tt>state</tt> attribute.
+ */
+ public void setState(State state)
+ {
+ ConferenceInfoDocument.this.setState(endpointElement, state);
+ }
+
+ /**
+ * Returns the value of the <tt>state</tt> attribute of this
+ * <tt>Endpoint</tt>'s element
+ * @return the value of the <tt>state</tt> attribute of this
+ * <tt>Endpoint</tt>'s element
+ */
+ public State getState()
+ {
+ return ConferenceInfoDocument.this.getState(endpointElement);
+ }
+
+ /**
+ * Sets the <tt>status</tt> child element of this <tt>Endpoint</tt>'s
+ * element.
+ * @param status the value to be used for the text content of the
+ * <tt>status</tt> element.
+ */
+ public void setStatus(EndpointStatusType status)
+ {
+ ConferenceInfoDocument.this.setStatus(endpointElement,
+ status == null
+ ? null
+ : status.toString());
+ }
+
+ /**
+ * Returns the <tt>EndpointStatusType</tt> corresponding to the
+ * <tt>status</tt> child of this <tt>Endpoint</tt>'s element, or
+ * <tt>null</tt>.
+ * @return the <tt>EndpointStatusType</tt> corresponding to the
+ * <tt>status</tt> child of this <tt>Endpoint</tt>'s element, or
+ * <tt>null</tt>.
+ */
+ public EndpointStatusType getStatus()
+ {
+ Element statusElement
+ = XMLUtils.findChild(endpointElement, STATUS_ELEMENT_NAME);
+ return statusElement == null
+ ? null
+ : EndpointStatusType.parseString(statusElement.getTextContent());
+ }
+
+ /**
+ * Returns the list of <tt>Media</tt>s which represent the
+ * <tt>media</tt> children of this <tt>Endpoint</tt>'s element.
+ * @return the list of <tt>Media</tt>s which represent the
+ * <tt>media</tt> children of this <tt>Endpoint</tt>'s element.
+ */
+ public List<Media> getMedias()
+ {
+ return mediasList;
+ }
+
+ /**
+ * Searches this <tt>Endpoint</tt>'s associated <tt>Media</tt>s
+ * and returns the one with <tt>id</tt> attribute <tt>id</tt>, or
+ * <tt>null</tt> if one wasn't found.
+ * @param id The value of the <tt>id</tt> attribute to search
+ * for.
+ * @return The <tt>Media</tt>s with <tt>id</tt> attribute <tt>id</tt>,
+ * or <tt>null</tt> if one wasn't found.
+ */
+ public Media getMedia(String id)
+ {
+ if (id == null)
+ return null;
+ for (Media m : mediasList)
+ {
+ if (id.equals(m.getId()))
+ return m;
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new <tt>Media</tt> instance, adds it to this
+ * <tt>Endpoint</tt> and returns it.
+ * @param id The value to use for the <tt>id</tt> attribute of the
+ * new <tt>Media</tt>'s element.
+ * @return the newly created <tt>Media</tt> instance.
+ */
+ public Media addNewMedia(String id)
+ {
+ Element mediaElement = document.createElement(MEDIA_ELEMENT_NAME);
+ Media media = new Media(mediaElement);
+ media.setId(id);
+
+ endpointElement.appendChild(mediaElement);
+ mediasList.add(media);
+
+ return media;
+ }
+
+ /**
+ * Removes a specific <tt>Media</tt> (the one with id <tt>id</tt>) from
+ * this <tt>Endpoint</tt>.
+ * @param id the <tt>id</tt> of the <tt>Media</tt> to remove.
+ */
+ public void removeMedia(String id)
+ {
+ Media media = getMedia(id);
+ if (media != null)
+ {
+ mediasList.remove(media);
+ endpointElement.removeChild(media.mediaElement);
+ }
+ }
+ }
+
+ /**
+ * Wraps around an <tt>Element</tt> and represents a <tt>media</tt>
+ * element. See RFC4575.
+ */
+ public class Media
+ {
+ /**
+ * The underlying <tt>Element</tt>.
+ */
+ private Element mediaElement;
+
+ /**
+ * Creates a new <tt>Media</tt> instance with the specified
+ * <tt>Element</tt> as its underlying element.
+ * @param media the <tt>Element</tt> to use
+ */
+ private Media(Element media)
+ {
+ this.mediaElement = media;
+ }
+
+ /**
+ * Sets the <tt>id</tt> attribute of this <tt>Media</tt>'s element to
+ * <tt>id</tt>
+ * @param id the value to set for the <tt>id</tt> attribute.
+ */
+ public void setId(String id)
+ {
+ mediaElement.setAttribute(ID_ATTR_NAME, id);
+ }
+
+ /**
+ * Returns the <tt>id</tt> attribute of this <tt>Media</tt>'s element.
+ * @return the <tt>id</tt> attribute of this <tt>Media</tt>'s element.
+ */
+ public String getId()
+ {
+ return mediaElement.getAttribute(ID_ATTR_NAME);
+ }
+
+ /**
+ * Sets the <tt>src-id</tt> child element of this <tt>Media</tt>'s
+ * element.
+ * @param srcId the value to be used for the text content of the
+ * <tt>src-id</tt> element.
+ */
+ public void setSrcId(String srcId)
+ {
+ Element srcIdElement
+ = XMLUtils.findChild(mediaElement, SRC_ID_ELEMENT_NAME);
+ if (srcIdElement == null)
+ {
+ srcIdElement
+ = document.createElement(SRC_ID_ELEMENT_NAME);
+ mediaElement.appendChild(srcIdElement);
+ }
+ srcIdElement.setTextContent(srcId);
+ }
+
+ /**
+ * Returns the text content of the <tt>src-id</tt> child element
+ * of this <tt>Media</tt>'s element, if it has such a child. Returns
+ * <tt>null</tt> otherwise.
+ * @return the text content of the <tt>src-id</tt> child element
+ * of this <tt>Media</tt>'s element, if it has such a child. Returns
+ * <tt>null</tt> otherwise.
+ */
+ public String getSrcId()
+ {
+ Element srcIdElement
+ = XMLUtils.findChild(mediaElement, SRC_ID_ELEMENT_NAME);
+ return srcIdElement == null
+ ? null
+ : srcIdElement.getTextContent();
+ }
+
+ /**
+ * Sets the <tt>type</tt> child element of this <tt>Media</tt>'s
+ * element.
+ * @param type the value to be used for the text content of the
+ * <tt>type</tt> element.
+ */
+ public void setType(String type)
+ {
+ Element typeElement
+ = XMLUtils.findChild(mediaElement, TYPE_ELEMENT_NAME);
+ if (typeElement == null)
+ {
+ typeElement = document.createElement(TYPE_ELEMENT_NAME);
+ mediaElement.appendChild(typeElement);
+ }
+ typeElement.setTextContent(type);
+ }
+
+ /**
+ * Returns the text content of the <tt>type</tt> child element
+ * of this <tt>Media</tt>'s element, if it has such a child. Returns
+ * <tt>null</tt> otherwise.
+ * @return the text content of the <tt>type</tt> child element
+ * of this <tt>Media</tt>'s element, if it has such a child. Returns
+ * <tt>null</tt> otherwise.
+ */
+ public String getType()
+ {
+ Element typeElement
+ = XMLUtils.findChild(mediaElement, TYPE_ELEMENT_NAME);
+ return typeElement == null
+ ? null
+ : typeElement.getTextContent();
+ }
+
+ /**
+ * Sets the <tt>status</tt> child element of this <tt>Media</tt>'s
+ * element.
+ * @param status the value to be used for the text content of the
+ * <tt>status</tt> element.
+ */
+ public void setStatus(String status)
+ {
+ ConferenceInfoDocument.this.setStatus(mediaElement, status);
+ }
+
+ /**
+ * Returns the text content of the <tt>status</tt> child element
+ * of this <tt>Media</tt>'s element, if it has such a child. Returns
+ * <tt>null</tt> otherwise.
+ * @return the text content of the <tt>status</tt> child element
+ * of this <tt>Media</tt>'s element, if it has such a child. Returns
+ * <tt>null</tt> otherwise.
+ */
+ public String getStatus()
+ {
+ Element statusElement
+ = XMLUtils.findChild(mediaElement, STATUS_ELEMENT_NAME);
+ return statusElement == null
+ ? null
+ : statusElement.getTextContent();
+ }
+ }
+
+ /**
+ * Endpoint status type.
+ *
+ * @author Sebastien Vincent
+ */
+ public enum EndpointStatusType
+ {
+ /**
+ * Pending.
+ */
+ pending("pending"),
+
+ /**
+ * Dialing-out.
+ */
+ dialing_out ("dialing-out"),
+
+ /**
+ * Dialing-in.
+ */
+ dialing_in("dialing-in"),
+
+ /**
+ * Alerting.
+ */
+ alerting("alerting"),
+
+ /**
+ * On-hold.
+ */
+ on_hold("on-hold"),
+
+ /**
+ * Connected.
+ */
+ connected("connected"),
+
+ /**
+ * Muted via focus.
+ */
+ muted_via_focus("mute-via-focus"),
+
+ /**
+ * Disconnecting.
+ */
+ disconnecting("disconnecting"),
+
+ /**
+ * Disconnected.
+ */
+ disconnected("disconnected");
+
+ /**
+ * The name of this type.
+ */
+ private final String type;
+
+ /**
+ * Creates a <tt>EndPointType</tt> instance with the specified name.
+ *
+ * @param type type name.
+ */
+ private EndpointStatusType(String type)
+ {
+ this.type = type;
+ }
+
+ /**
+ * Returns the type name.
+ *
+ * @return type name
+ */
+ @Override
+ public String toString()
+ {
+ return type;
+ }
+
+ /**
+ * Returns a <tt>EndPointType</tt>.
+ *
+ * @param typeStr the <tt>String</tt> that we'd like to
+ * parse.
+ * @return an EndPointType.
+ *
+ * @throws IllegalArgumentException in case <tt>typeStr</tt> is
+ * not a valid <tt>EndPointType</tt>.
+ */
+ public static EndpointStatusType parseString(String typeStr)
+ throws IllegalArgumentException
+ {
+ for (EndpointStatusType value : values())
+ if (value.toString().equals(typeStr))
+ return value;
+
+ throw new IllegalArgumentException(
+ typeStr + " is not a valid reason");
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java
index 36017d7..5efa7ca 100644
--- a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java
+++ b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java
@@ -33,6 +33,7 @@ import org.jitsi.service.neomedia.event.*;
*
* @author Emil Ivov
* @author Lyubomir Marinov
+ * @author Boris Grozev
*/
public abstract class MediaAwareCallPeer
<T extends MediaAwareCall<?, ?, V>,
@@ -122,10 +123,32 @@ public abstract class MediaAwareCallPeer
= new LinkedList<PropertyChangeListener>();
/**
+ * Represents the last Conference Information (RFC4575) document sent to
+ * this <tt>CallPeer</tt>. This is always a document with state "full", even
+ * if the last document actually sent was a "partial"
+ */
+ private ConferenceInfoDocument lastConferenceInfoSent = null;
+
+ /**
+ * The time (as obtained by <tt>System.currentTimeMillis()</tt>) at which
+ * a Conference Information (RFC4575) document was last sent to this
+ * <tt>CallPeer</tt>.
+ */
+ private long lastConferenceInfoSentTimestamp = -1;
+
+ /**
+ * The last Conference Information (RFC4575) document sent to us by this
+ * <tt>CallPeer</tt>. This is always a document with state "full", which is
+ * only gets updated by "partial" or "deleted" documents.
+ */
+ private ConferenceInfoDocument lastConferenceInfoReceived = null;
+
+ /**
* Creates a new call peer with address <tt>peerAddress</tt>.
*
* @param owningCall the call that contains this call peer.
*/
+
public MediaAwareCallPeer(T owningCall)
{
this.call = owningCall;
@@ -1003,4 +1026,97 @@ public abstract class MediaAwareCallPeer
}
}
}
+
+ /**
+ * Returns the last <tt>ConferenceInfoDocument</tt> sent by us to this
+ * <tt>CallPeer</tt>. It is a document with state <tt>full</tt>
+ * @return the last <tt>ConferenceInfoDocument</tt> sent by us to this
+ * <tt>CallPeer</tt>. It is a document with state <tt>full</tt>
+ */
+ public ConferenceInfoDocument getLastConferenceInfoSent()
+ {
+ return lastConferenceInfoSent;
+ }
+
+ /**
+ * Sets the last <tt>ConferenceInfoDocument</tt> sent by us to this
+ * <tt>CallPeer</tt>.
+ * @param confInfo the document to set.
+ */
+ public void setLastConferenceInfoSent(ConferenceInfoDocument confInfo)
+ {
+ lastConferenceInfoSent = confInfo;
+ }
+
+ /**
+ * Gets the time (as obtained by <tt>System.currentTimeMillis()</tt>)
+ * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
+ * <tt>CallPeer</tt>.
+ * @return the time (as obtained by <tt>System.currentTimeMillis()</tt>)
+ * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
+ * <tt>CallPeer</tt>.
+ */
+ public long getLastConferenceInfoSentTimestamp()
+ {
+ return lastConferenceInfoSentTimestamp;
+ }
+
+ /**
+ * Sets the time (as obtained by <tt>System.currentTimeMillis()</tt>)
+ * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
+ * <tt>CallPeer</tt>.
+ * @param newTimestamp the time to set
+ */
+ public void setLastConferenceInfoSentTimestamp(long newTimestamp)
+ {
+ lastConferenceInfoSentTimestamp = newTimestamp;
+ }
+
+ /**
+ * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this
+ * <tt>CallPeer</tt>.
+ * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this
+ * <tt>CallPeer</tt>.
+ */
+ public ConferenceInfoDocument getLastConferenceInfoReceived()
+ {
+ return lastConferenceInfoReceived;
+ }
+
+ /**
+ * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this
+ * <tt>CallPeer</tt>.
+ * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this
+ * <tt>CallPeer</tt>.
+ */
+ public void setLastConferenceInfoReceived(ConferenceInfoDocument confInfo)
+ {
+ lastConferenceInfoReceived = confInfo;
+ }
+
+ /**
+ * Gets the <tt>version</tt> of the last <tt>ConferenceInfoDocument</tt>
+ * sent to us by this <tt>CallPeer</tt>, or -1 if we haven't (yet) received
+ * a <tt>ConferenceInformationDocument</tt> from this <tt>CallPeer</tt>.
+ * @return
+ */
+ public int getLastConferenceInfoReceivedVersion()
+ {
+ return (lastConferenceInfoReceived == null)
+ ? -1
+ : lastConferenceInfoReceived.getVersion();
+ }
+
+ /**
+ * Gets the <tt>String</tt> to be used for this <tt>CallPeer</tt> when
+ * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the
+ * <tt>entity</tt> key attribute which to use for the <tt>user</tt>
+ * element corresponding to this <tt>CallPeer</tt>)
+ *
+ * @return the <tt>String</tt> to be used for this <tt>CallPeer</tt> when
+ * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the
+ * <tt>entity</tt> key attribute which to use for the <tt>user</tt>
+ * element corresponding to this <tt>CallPeer</tt>)
+ */
+ public abstract String getEntity();
}
diff --git a/src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf b/src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf
index fe14a18..9ecfd1d 100644
--- a/src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf
+++ b/src/net/java/sip/communicator/service/protocol/media/protocol.media.manifest.mf
@@ -5,6 +5,9 @@ Bundle-Vendor: jitsi.org
Bundle-Version: 0.0.1
System-Bundle: yes
Import-Package: javax.xml.parsers,
+ javax.xml.transform,
+ javax.xml.transform.dom,
+ javax.xml.transform.stream,
net.java.sip.communicator.service.netaddr,
net.java.sip.communicator.service.protocol,
net.java.sip.communicator.service.protocol.event,
@@ -20,6 +23,7 @@ Import-Package: javax.xml.parsers,
org.jitsi.service.protocol,
org.jitsi.util,
org.jitsi.util.event,
+ org.jitsi.util.xml,
org.osgi.framework,
org.w3c.dom,
org.xml.sax