diff options
author | Boris Grozev <boris@jitsi.org> | 2013-06-27 18:42:20 +0300 |
---|---|---|
committer | Boris Grozev <boris@jitsi.org> | 2013-06-27 18:42:20 +0300 |
commit | 4643383af893c51664063fe249a024ca7b8398be (patch) | |
tree | 568e3d20aaeb9546fecbf226ab95f54409083187 | |
parent | 906af90a0423ad03620c1b761b674958d9d31428 (diff) | |
parent | 6e56eb79858c869bb450d597ee08831b271828a1 (diff) | |
download | jitsi-4643383af893c51664063fe249a024ca7b8398be.zip jitsi-4643383af893c51664063fe249a024ca7b8398be.tar.gz jitsi-4643383af893c51664063fe249a024ca7b8398be.tar.bz2 |
Merge branch 'coin-refactor'
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 |