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 e648b1882..d7cbc1fa5 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 @@ protected void sendCandidates( 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 2bd1610a1..639a13f07 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java @@ -71,6 +71,17 @@ public class CallPeerJabberImpl */ private final Object sidSyncRoot = new Object(); + /** + * Whether a COIN has been scheduled to be sent to this + * CallPeerJabberImpl + */ + private boolean coinScheduled = false; + + /** + * Synchronization object for coinScheduled + */ + private final Object coinScheduledSyncRoot = new Object(); + /** * Creates a new call peer with address peerAddress. * @@ -1453,4 +1464,40 @@ else if (((IQ) result).getType() != IQ.Type.RESULT) new TransferredPacketExtension())); } } + + /** + * Check whether a COIN is scheduled to be sent to this CallPeer + * (i.e. there is a thread which will eventually (after sleeping a certain + * amount of time) trigger a COIN to be sent) + * @return true if there is a COIN scheduled to be sent to this + * CallPeer and false otherwise + */ + public boolean isCoinScheduled() + { + synchronized (coinScheduledSyncRoot) + { + return coinScheduled; + } + } + + /** + * Sets the property which indicates whether a COIN is scheduled to be sent + * to this CallPeer. + * @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 34ebaa574..9be9aeef4 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.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 @@ * * @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 + * CallPeer. */ - private final Object lock = new Object(); + private static final int COIN_MIN_INTERVAL = 200; /** - * The value of the version attribute to be specified in the - * outgoing conference-info root XML elements. + * Synchronization object. */ - private int version = 1; + private final Object lock = new Object(); /** * Initializes a new OperationSetTelephonyConferencingJabberImpl @@ -97,8 +98,6 @@ protected void notifyCallPeers(Call call) { notify(i.next()); } - - version++; } } } @@ -114,10 +113,41 @@ private void notify(CallPeer callPeer) 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 @@ private void notify(CallPeer callPeer) ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_COIN)) { logger.info(callPeer.getAddress() + " does not support COIN"); + callPeerJabber.setCoinScheduled(false); return; } } @@ -135,135 +166,41 @@ private void notify(CallPeer callPeer) 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 CallPeerJabberImpl. - * - * @param callPeer CallPeer - * @param remote if the callPeer is remote or local - * @return list of media packet extension - */ - private List getMedia( - MediaAwareCallPeer callPeer, - boolean remote) - { - CallPeerMediaHandler mediaHandler = callPeer.getMediaHandler(); - List ret = new ArrayList(); - 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 CallPeerJabberImpl. - * - * @param callPeer CallPeer - * @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 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 status XML element - * of an endpoint XML element and which describes the state of a - * specific CallPeer. - * - * @param callPeer the CallPeer which is to get its state described - * in a status XML element of an endpoint XML element - * @return the text content to be put in the status XML element of - * an endpoint XML element and which describes the state of the - * specified callPeer - */ - 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 @@ private EndpointStatusType getEndpointStatus(CallPeer callPeer) * conference managed by the local peer. * * @param callPeer the CallPeer to generate conference-info XML for - * @param version the value of the version attribute of the - * conference-info root element of the conference-info XML to be - * generated + * @param confInfo the ConferenceInformationDocument which is to be + * included in the IQ * @return the conference-info IQ to be sent to the specified * callPeer 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 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 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 void processPacket(Packet packet) 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 void processPacket(Packet packet) */ 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 + * conference-info 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 1d618cb15..c4a16969a 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 b0963b7cc..874225f1b 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 @@ private void setDisconnectedState(boolean failed, String reason) 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 596b92b75..8202f1958 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.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 OperationSetTelephonyConferencing for SIP. * * @author Lyubomir Marinov + * @author Boris Grozev */ public class OperationSetTelephonyConferencingSipImpl extends AbstractOperationSetTelephonyConferencing< @@ -40,7 +38,6 @@ public class OperationSetTelephonyConferencingSipImpl Address> implements MethodProcessorListener { - /** * The Logger used by the * OperationSetTelephonyConferencingSipImpl class and its instances @@ -55,28 +52,6 @@ public class OperationSetTelephonyConferencingSipImpl */ private static final String CONTENT_SUB_TYPE = "conference-info+xml"; - /** - * The name of the conference-info XML element - * conference-description. - */ - private static final String ELEMENT_CONFERENCE_DESCRIPTION - = "conference-description"; - - /** - * The name of the conference-info XML element conference-info. - */ - private static final String ELEMENT_CONFERENCE_INFO = "conference-info"; - - /** - * The name of the conference-info XML element conference-state. - */ - private static final String ELEMENT_CONFERENCE_STATE = "conference-state"; - - /** - * The name of the conference-info XML element user-count. - */ - private static final String ELEMENT_USER_COUNT = "user-count"; - /** * The name of the event package supported by * OperationSetTelephonyConferencingSipImpl in SUBSCRIBE and NOTIFY @@ -98,12 +73,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 EventPackageNotifier which implements conference * event-package notifier support on behalf of this @@ -283,267 +252,44 @@ protected CallSipImpl createOutgoingCall() /** * Generates the conference-info XML to be sent to a specific * CallPeer in order to notify it of the current state of the - * conference managed by the local peer. + * conference managed by the local peer. Return null if + * conference-info XML does not need to be sent to callPeer. * * @param callPeer the CallPeer to generate conference-info XML for - * @param version the value of the version attribute of the * conference-info root element of the conference-info XML to be * generated * @return the conference-info XML to be sent to the specified * callPeer 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( "\r\n"); - // - append(xml, "<", ELEMENT_CONFERENCE_INFO); - // entity - append(xml, " entity=\"", domElementWriter.encode(localParty), "\""); - // state - xml.append(" state=\"full\""); - // version - append(xml, " version=\"", Integer.toString(version), "\">"); - // - append(xml, "<", ELEMENT_CONFERENCE_DESCRIPTION, "/>"); - // - append(xml, "<", ELEMENT_CONFERENCE_STATE, ">"); - // - append(xml, "<", ELEMENT_USER_COUNT, ">"); - - CallSipImpl call = callPeer.getCall(); - List conferenceCallPeers = CallConference.getCallPeers(call); - - xml.append(1 /* the local peer/user */ + conferenceCallPeers.size()); - // - append(xml, ""); - // - append(xml, ""); - // - append(xml, "<", ELEMENT_USERS, ">"); - - // - append(xml, "<", ELEMENT_USER); - // entity - append(xml, " entity=\"", domElementWriter.encode(localParty), "\""); - // state - xml.append(" state=\"full\">"); - - String ourDisplayName = parentProvider.getOurDisplayName(); - - if (ourDisplayName != null) - { - // - append(xml, "<", ELEMENT_DISPLAY_TEXT, ">"); - xml.append(domElementWriter.encode(ourDisplayName)); - // - append(xml, ""); - } - // - append(xml, "<", ELEMENT_ENDPOINT, ">"); - // - append(xml, "<", ELEMENT_STATUS, ">"); - // We are the conference focus so we're connected to the conference. - xml.append(AbstractConferenceMember.CONNECTED); - // - append(xml, ""); - getMediaXML(callPeer, false, xml); - // - append(xml, ""); - // - append(xml, ""); - - for (CallPeer conferenceCallPeer : conferenceCallPeers) - getUserXML(conferenceCallPeer, xml); - - // - append(xml, ""); - // - append(xml, ""); - return xml.toString(); - } - - /** - * Generates the text content to be put in the status XML element - * of an endpoint XML element and which describes the state of a - * specific CallPeer. - * - * @param callPeer the CallPeer which is to get its state described - * in a status XML element of an endpoint XML element - * @return the text content to be put in the status XML element of - * an endpoint XML element and which describes the state of the - * specified callPeer - */ - 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 StringBuffer media XML element - * trees which describe the state of the media streaming between a specific - * CallPeer and its local peer represented by an associated - * Call. - * - * @param callPeer the CallPeer which is to get its media streaming - * state described in media XML element trees appended to the - * specified StringBuffer - * @param remote true if the streaming from the callPeer - * to the local peer is to be described or false if the streaming - * from the local peer to the remote callPeer is to be described - * @param xml the StringBuffer to append the media XML - * trees describing the media streaming state of the specified - * callPeer - */ - 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) - { - // - append(xml, "<", ELEMENT_MEDIA, ">"); - // - append(xml, "<", ELEMENT_TYPE, ">"); - xml.append(mediaType.toString()); - // - append(xml, ""); - - long srcId - = remote - ? getRemoteSourceID(callPeer, mediaType) - : stream.getLocalSourceID(); - - if (srcId != -1) - { - // - append(xml, "<", ELEMENT_SRC_ID, ">"); - xml.append(srcId); - // - append(xml, ""); - } - - MediaDirection direction - = remote - ? getRemoteDirection(callPeer, mediaType) - : stream.getDirection(); - - if (direction == null) - direction = MediaDirection.INACTIVE; - - // - append(xml, "<", ELEMENT_STATUS, ">"); - xml.append(direction.toString()); - // - append(xml, ""); - // - append(xml, ""); - } - } - } - - /** - * Appends to a specific StringBuffer a user XML element - * tree which describes the participation of a specific CallPeer in - * a conference managed by the local peer represented by its associated - * Call. - * - * @param callPeer the CallPeer which is to get its conference - * participation describes in a user XML element tree appended to - * the specified StringBuffer - * @param xml the StringBuffer to append the user XML - * tree describing the conference participation of the specified - * callPeer to + * conference managed by the local peer. Return null if + * conference-info XML does not need to be sent to callPeer. */ - private void getUserXML(CallPeer callPeer, StringBuffer xml) + private String getConferenceInfoXML(CallPeerSipImpl callPeer) { - // - 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) - { - // - append(xml, "<", ELEMENT_DISPLAY_TEXT, ">"); - xml.append(domElementWriter.encode(displayName)); - // - append(xml, ""); - } - // - 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 { - // - append(xml, "<", ELEMENT_STATUS, ">"); - xml.append(status); - // - append(xml, ""); + 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); - // - append(xml, ""); - // - append(xml, ""); } /** @@ -552,7 +298,7 @@ private void getUserXML(CallPeer callPeer, StringBuffer xml) * Implements the protocol-dependent part of the logic of inviting a callee * to a Call. The protocol-independent part of that logic is * implemented by - * {@link AbstractOperationSetTelephonyConferencing#inviteCalleToCall(String,Call)}. + * {@link AbstractOperationSetTelephonyConferencing#inviteCalleeToCall(String,Call)}. */ @Override protected CallPeerSipImpl doInviteCalleeToCall( @@ -778,6 +524,37 @@ public void responseProcessed( } } + /** + * {@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 EventPackageNotifier.Subscription in order to * represent a conference subscription created by a remote CallPeer @@ -786,13 +563,6 @@ public void responseProcessed( private class ConferenceNotifierSubscription extends EventPackageNotifier.Subscription { - - /** - * The value of the version attribute to be specified in the - * outgoing conference-info root XML elements. - */ - private int version = 1; - /** * Initializes a new ConferenceNotifierSubscription instance * with a specific subscription Address/Request URI and a @@ -845,29 +615,52 @@ protected byte[] createNotifyContent( 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 @@ protected void processActiveRequest( { 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 37be06b43..e9a10ad78 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 @@ * @param * * @author Lyubomir Marinov + * @author Boris Grozev */ public abstract class AbstractOperationSetTelephonyConferencing< ProtocolProviderServiceT extends ProtocolProviderService, @@ -748,21 +746,21 @@ else if (RegistrationState.UNREGISTERED.equals(newState)) /** * Updates the conference-related properties of a specific CallPeer * such as conferenceFocus and conferenceMembers with - * information received from it as a conference focus in the form of a - * conference-info XML document. + * the information described in confInfo. + * confInfo must be a document with "full" state. * * @param callPeer the CallPeer which is a conference focus and has * sent the specified conference-info XML document - * @param conferenceInfoDocument the conference-info XML document sent by - * callPeer in order to update the conference-related information - * of the local peer represented by the associated Call + * @param confInfo the conference-info XML document to use to update + * the conference-related information of the local peer represented + * by the associated Call. 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 @@ else if (ELEMENT_ENDPOINT.equals(userChildName)) if (changed) notifyAll(callPeer.getCall()); + + callPeer.setLastConferenceInfoReceived(confInfo); + return confInfo.getVersion(); } /** @@ -898,9 +899,6 @@ else if (ELEMENT_ENDPOINT.equals(userChildName)) * * @param callPeer the CallPeer which is a conference focus and has * sent the specified conference-info XML document - * @param version the value of the version attribute of the - * conference-info XML element currently represented in the - * specified callPeer * @param conferenceInfoXML the conference-info XML document sent by * callPeer in order to update the conference-related information * of the local peer represented by the associated Call @@ -908,89 +906,566 @@ else if (ELEMENT_ENDPOINT.equals(userChildName)) * conference-info XML element of the specified * conferenceInfoXML if it was successfully parsed and represented * in the specified callPeer + * + * @throws XMLException If conferenceInfoXML couldn't be parsed as + * a ConferenceInfoDocument */ 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 String if any are present in it. + * + * @param address the String value representing an address from + * which any parameters are to be removed + * @return a String representing the specified address + * 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 ConferenceInfoDocument which describes the current + * state of the conference in which callPeer participates. The + * created document contains a "full" description (as opposed to a partial + * description, see RFC4575). + * + * @return a ConferenceInfoDocument which describes the current + * state of the conference in which this CallPeer 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 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 user element to confInfo which describes + * callPeer, or the local peer if remote is false. + * + * @param confInfo the ConferenceInformationDocument to which to + * add a user element + * @param callPeer the CallPeer which should be described + * @param remote true to describe callPeer, or + * false 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 entity attribute of the + * user element for the local peer, in a Conference Information + * document to be sent to callPeer + * + * @param callPeer The CallPeer for which we are creating a + * Conference Information document. + * @return a string to be used for the entity attribute of the + * user element for the local peer, in a Conference Information + * document to be sent to callPeer + */ + 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 EndpointStatusType to use when describing + * callPeer in a Conference Information document. + * + * @param callPeer the CallPeer which is to get its state described + * in a status XML element of an endpoint XML element + * @return the EndpointStatusType to use when describing + * callPeer 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 full from which to generate a + * "diff". + * @param to A document with state full to which to generate a + * "diff" + * @return a ConferenceInfoDocument, such that when it is applied + * to from using the procedure defined in section 4.6 of RFC4575, + * the result is to. May return null if from and + * to 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 CallPeer + * such as conferenceFocus and conferenceMembers with + * information received from it as a conference focus in the form of a + * partial conference-info XML document. + * + * @param callPeer the CallPeer 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 + * callPeer in order to update the conference-related information + * of the local peer represented by the associated Call + * @return the value of the version attribute of the + * conference-info XML element of the specified + * conferenceInfoXML if it was successfully parsed and represented + * in the specified callPeer + */ + 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 String if any are present in it. + * @param a A document with state full which to compare to + * b + * @param b A document with state full which to compare to + * a + * @return false if the two documents are found to be different, + * true 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 ConferenceInfoDocument.User instances + * match according to the needs of our implementation. Can return + * true for users which are not identical. * - * @param address the String value representing an address from - * which any parameters are to be removed - * @return a String representing the specified address - * without any parameters + * @param a A ConferenceInfoDocument.User to compare + * @param b A ConferenceInfoDocument.User to compare + * @return false if a and b are found to be + * different in a way that is significant for our needs, true + * 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 ConferenceInfoDocument.Endpoint instances + * match according to the needs of our implementation. Can return + * true for endpoints which are not identical. + * + * @param a A ConferenceInfoDocument.Endpoint to compare + * @param b A ConferenceInfoDocument.Endpoint to compare + * @return false if a and b are found to be + * different in a way that is significant for our needs, true + * 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 ConferenceInfoDocument.Media instances + * match according to the needs of our implementation. Can return + * true for endpoints which are not identical. + * + * @param a A ConferenceInfoDocument.Media to compare + * @param b A ConferenceInfoDocument.Media to compare + * @return false if a and b are found to be + * different in a way that is significant for our needs, true + * 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 String to compare to b + * @param b A String to compare to a + * @return true if and only if a and b are both + * null, or they are equal as Strings + */ + 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 000000000..39bec2b35 --- /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 Document providing convenience + * functions. + * + * {@link "http://tools.ietf.org/html/rfc4575"} + * + * @author Boris Grozev + * @author Sebastien Vincent + */ +public class ConferenceInfoDocument +{ + /** + * The Logger used by the ConferenceInfoDocument 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 Document object that we wrap around. + */ + private Document document; + + /** + * The single conference-info element of document + */ + private Element conferenceInfo; + + /** + * The conference-description child element of + * conference-info. + */ + private Element conferenceDescription; + + /** + * The conference-state child element of conference-info. + */ + private Element conferenceState; + + /** + * The conference-state child element of conference-state. + */ + private Element userCount; + + /** + * The users child element of conference-info. + */ + private Element users; + + /** + * A list of Users representing the children of users + */ + private final List usersList = new LinkedList(); + + /** + * Creates a new ConferenceInfoDocument 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 ConferenceInfoDocument instance and populates it + * by parsing the XML in xml + * + * @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; iversion attribute of the + * conference-info element, or -1 if there is no version + * attribute or if it's value couldn't be parsed as an integer. + * @return the value of the version attribute of the + * conference-info element, or -1 if there is no version + * 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 version attribute of the conference-info + * element. + * @param version the value to set the version attribute of the + * conference-info element to. + */ + public void setVersion(int version) + { + conferenceInfo.setAttribute(VERSION_ATTR_NAME, Integer.toString(version)); + } + + /** + * Gets the value of the state attribute of the + * conference-info element. + * @return the value of the state attribute of the + * conference-info element. + */ + public State getState() + { + return getState(conferenceInfo); + } + + /** + * Returns the value of the state attribute of the users + * child of the conference-info element. + * + * @return the value of the state attribute of the users + * child of the conference-info element. + */ + public State getUsersState() + { + return getState(users); + } + + /** + * Sets the value of the state attribute of the + * conference-info element. + * @param state the value to set the state attribute of the + * conference-info element to. + */ + public void setState(State state) + { + setState(conferenceInfo, state); + } + + /** + * Sets the value of the sid attribute of the + * conference-info 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 sid attribute of the + * conference-info element to. + */ + public void setSid(String sid) + { + conferenceInfo.setAttribute("sid", sid); + } + + /** + * Sets the value of the entity attribute of the + * conference-info element. + * @param entity the value to set the entity attribute of the + * conference-info document to. + */ + public void setEntity(String entity) + { + conferenceInfo.setAttribute(ENTITY_ATTR_NAME, entity); + } + + /** + * Gets the value of the entity attribute of the + * conference-info element. + * @return The value of the entity attribute of the + * conference-info element. + */ + public String getEntity() + { + return conferenceInfo.getAttribute(ENTITY_ATTR_NAME); + } + + /** + * Sets the content of the user-count child element of the + * conference-state child element of conference-info + * @param count the value to set the content of user-count 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 user-count child of the + * conference-state child of conference-info, parsed as + * an integer, if they exist. Returns -1 if either there isn't a + * conference-state element, it doesn't have a user-count + * child, or parsing as integer failed. + * + * @return the content of the user-count child of the + * conference-state child of conference-info 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 conference-info tree, + * or null if an error occurs while trying to get it. + * + * @return the XML representation of the conference-info tree, + * or null 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 + * conference-info 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 User that represents the user + * children of the users child element of conference-info + * @return the list of User that represents the user + * children of the users child element of conference-info + */ + public List getUsers() + { + return usersList; + } + + /** + * Searches this document's Users and returns the one with + * entity attribute entity, or null if one + * wasn't found. + * @param entity The value of the entity attribute to search for. + * @return the User of this document with entity + * attribute entity, or null 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 User instance, adds it to the document and + * returns it. + * @param entity The value to use for the entity attribute of the + * new User. + * @return the newly created User 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 User (the one with entity entity) + * from the document. + * @param entity the entity of the User to remove. + */ + public void removeUser(String entity) + { + User user = getUser(entity); + if (user != null) + { + usersList.remove(user); + users.removeChild(user.userElement); + } + } + + /** + * Returns the Document that this instance wraps around. + * @return the Document that this instance wraps around. + */ + public Document getDocument() + { + return document; + } + + /** + * Returns the State corresponding to the state attribute + * of an Element. Default to State.FULL which is the + * RFC4575 default. + * @param element the Element + * @return the State corresponding to the state attribute + * of an Element. + */ + 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 element to state. + * If state is State.FULL removes the "state" attribute, + * because this is the default value. + * @param element The Element for which to set the "state" + * attribute of. + * @param state the State 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 status child element of element. If + * statusString is null, the child element is removed + * if present. + * @param element the Element for which to set the status + * child element. + * @param statusString the String to use for the text content of + * the status 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 state attribute (see + * RFC4575) + */ + public enum State + { + /** + * State full + */ + FULL("full"), + + /** + * State partial + */ + PARTIAL("partial"), + + /** + * State deleted + */ + DELETED("deleted"); + + /** + * The name of this State + */ + private String name; + + /** + * Creates a State instance with the specified name. + * @param name + */ + private State(String name) + { + this.name = name; + } + + /** + * Returns the name of this State + * @return the name of this State + */ + @Override + public String toString() + { + return name; + } + + /** + * Returns a State value corresponding to the specified + * name + * @return a State value corresponding to the specified + * name + */ + 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 Element and represents a user + * element (child of the users element). See RFC4575. + */ + public class User + { + /** + * The underlying Element. + */ + private Element userElement; + + /** + * The list of Endpoints representing the endpoint + * children of this User's element. + */ + private List endpointsList = new LinkedList(); + + /** + * Creates a new User instance with the specified + * Element as its underlying element. + * @param user the Element to use + */ + private User(Element user) + { + this.userElement = user; + NodeList endpointsNodeList + = user.getElementsByTagName(ENDPOINT_ELEMENT_NAME); + for (int i=0; ientity attribute of this User's element + * to entity + * @param entity the value to set for the entity attribute. + */ + public void setEntity(String entity) + { + userElement.setAttribute(ENTITY_ATTR_NAME, entity); + } + + /** + * Returns the value of the entity attribute of this + * User's element. + * @return the value of the entity attribute of this + * User's element. + */ + public String getEntity() + { + return userElement.getAttribute(ENTITY_ATTR_NAME); + } + + /** + * Sets the state attribute of this User's element to + * state + * @param state the value to use for the state attribute. + */ + public void setState(State state) + { + ConferenceInfoDocument.this.setState(userElement, state); + } + + /** + * Returns the value of the state attribute of this + * User's element + * @return the value of the state attribute of this + * User's element + */ + public State getState() + { + return ConferenceInfoDocument.this.getState(userElement); + } + + /** + * Sets the display-text child element to this User's + * element. + * @param text the text content to use for the display-text + * 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 display-text child element + * of this User's element, if it has such a child. Returns + * null otherwise. + * @return the text content of the display-text child element + * of this User's element, if it has such a child. Returns + * null 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 Endpoints which represent the + * endpoint children of this User's element. + * @return the list of Endpoints which represent the + * endpoint children of this User's element. + */ + public List getEndpoints() + { + return endpointsList; + } + + /** + * Searches this User's associated Endpoints + * and returns the one with entity attribute entity, + * or null if one wasn't found. + * @param entity The value of the entity attribute to search + * for. + * @return The Endpoint with entity attribute + * entity, or null 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 Endpoint instance, adds it to this + * User and returns it. + * @param entity The value to use for the entity attribute of + * the new Endpoint. + * @return the newly created Endpoint 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 Endpoint (the one with entity + * entity) from this User. + * @param entity the entity of the Endpoint to remove + */ + public void removeEndpoint(String entity) + { + Endpoint endpoint = getEndpoint(entity); + if (endpoint != null) + { + endpointsList.remove(endpoint); + userElement.removeChild(endpoint.endpointElement); + } + } + } + + /** + * Wraps around an Element and represents an endpoint + * element. See RFC4575. + */ + public class Endpoint + { + /** + * The underlying Element. + */ + private Element endpointElement; + + /** + * The list of Medias representing the media + * children elements of this Endpoint's element. + */ + private List mediasList = new LinkedList(); + + /** + * Creates a new Endpoint instance with the specified + * Element as its underlying element. + * @param endpoint the Element to use + */ + private Endpoint(Element endpoint) + { + this.endpointElement = endpoint; + NodeList mediaNodeList + = endpoint.getElementsByTagName(MEDIA_ELEMENT_NAME); + for (int i=0; ientity attribute of this Endpoint's + * element to entity + * @param entity the value to set for the entity attribute. + */ + public void setEntity(String entity) + { + endpointElement.setAttribute(ENTITY_ATTR_NAME, entity); + } + + /** + * Returns the entity attribute of this Endpoint's + * element. + * @return the entity attribute of this Endpoint's + * element. + */ + public String getEntity() + { + return endpointElement.getAttribute(ENTITY_ATTR_NAME); + } + + /** + * Sets the state attribute of this User's element to + * state + * @param state the value to use for the state attribute. + */ + public void setState(State state) + { + ConferenceInfoDocument.this.setState(endpointElement, state); + } + + /** + * Returns the value of the state attribute of this + * Endpoint's element + * @return the value of the state attribute of this + * Endpoint's element + */ + public State getState() + { + return ConferenceInfoDocument.this.getState(endpointElement); + } + + /** + * Sets the status child element of this Endpoint's + * element. + * @param status the value to be used for the text content of the + * status element. + */ + public void setStatus(EndpointStatusType status) + { + ConferenceInfoDocument.this.setStatus(endpointElement, + status == null + ? null + : status.toString()); + } + + /** + * Returns the EndpointStatusType corresponding to the + * status child of this Endpoint's element, or + * null. + * @return the EndpointStatusType corresponding to the + * status child of this Endpoint's element, or + * null. + */ + public EndpointStatusType getStatus() + { + Element statusElement + = XMLUtils.findChild(endpointElement, STATUS_ELEMENT_NAME); + return statusElement == null + ? null + : EndpointStatusType.parseString(statusElement.getTextContent()); + } + + /** + * Returns the list of Medias which represent the + * media children of this Endpoint's element. + * @return the list of Medias which represent the + * media children of this Endpoint's element. + */ + public List getMedias() + { + return mediasList; + } + + /** + * Searches this Endpoint's associated Medias + * and returns the one with id attribute id, or + * null if one wasn't found. + * @param id The value of the id attribute to search + * for. + * @return The Medias with id attribute id, + * or null 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 Media instance, adds it to this + * Endpoint and returns it. + * @param id The value to use for the id attribute of the + * new Media's element. + * @return the newly created Media 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 Media (the one with id id) from + * this Endpoint. + * @param id the id of the Media to remove. + */ + public void removeMedia(String id) + { + Media media = getMedia(id); + if (media != null) + { + mediasList.remove(media); + endpointElement.removeChild(media.mediaElement); + } + } + } + + /** + * Wraps around an Element and represents a media + * element. See RFC4575. + */ + public class Media + { + /** + * The underlying Element. + */ + private Element mediaElement; + + /** + * Creates a new Media instance with the specified + * Element as its underlying element. + * @param media the Element to use + */ + private Media(Element media) + { + this.mediaElement = media; + } + + /** + * Sets the id attribute of this Media's element to + * id + * @param id the value to set for the id attribute. + */ + public void setId(String id) + { + mediaElement.setAttribute(ID_ATTR_NAME, id); + } + + /** + * Returns the id attribute of this Media's element. + * @return the id attribute of this Media's element. + */ + public String getId() + { + return mediaElement.getAttribute(ID_ATTR_NAME); + } + + /** + * Sets the src-id child element of this Media's + * element. + * @param srcId the value to be used for the text content of the + * src-id 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 src-id child element + * of this Media's element, if it has such a child. Returns + * null otherwise. + * @return the text content of the src-id child element + * of this Media's element, if it has such a child. Returns + * null otherwise. + */ + public String getSrcId() + { + Element srcIdElement + = XMLUtils.findChild(mediaElement, SRC_ID_ELEMENT_NAME); + return srcIdElement == null + ? null + : srcIdElement.getTextContent(); + } + + /** + * Sets the type child element of this Media's + * element. + * @param type the value to be used for the text content of the + * type 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 type child element + * of this Media's element, if it has such a child. Returns + * null otherwise. + * @return the text content of the type child element + * of this Media's element, if it has such a child. Returns + * null otherwise. + */ + public String getType() + { + Element typeElement + = XMLUtils.findChild(mediaElement, TYPE_ELEMENT_NAME); + return typeElement == null + ? null + : typeElement.getTextContent(); + } + + /** + * Sets the status child element of this Media's + * element. + * @param status the value to be used for the text content of the + * status element. + */ + public void setStatus(String status) + { + ConferenceInfoDocument.this.setStatus(mediaElement, status); + } + + /** + * Returns the text content of the status child element + * of this Media's element, if it has such a child. Returns + * null otherwise. + * @return the text content of the status child element + * of this Media's element, if it has such a child. Returns + * null 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 EndPointType 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 EndPointType. + * + * @param typeStr the String that we'd like to + * parse. + * @return an EndPointType. + * + * @throws IllegalArgumentException in case typeStr is + * not a valid EndPointType. + */ + 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 36017d7ac..5efa7caf2 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 @@ * * @author Emil Ivov * @author Lyubomir Marinov + * @author Boris Grozev */ public abstract class MediaAwareCallPeer , @@ -121,11 +122,33 @@ public abstract class MediaAwareCallPeer private final List videoPropertyChangeListeners = new LinkedList(); + /** + * Represents the last Conference Information (RFC4575) document sent to + * this CallPeer. 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 System.currentTimeMillis()) at which + * a Conference Information (RFC4575) document was last sent to this + * CallPeer. + */ + private long lastConferenceInfoSentTimestamp = -1; + + /** + * The last Conference Information (RFC4575) document sent to us by this + * CallPeer. 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 peerAddress. * * @param owningCall the call that contains this call peer. */ + public MediaAwareCallPeer(T owningCall) { this.call = owningCall; @@ -1003,4 +1026,97 @@ public void setState(CallPeerState newState, String reason, int reasonCode) } } } + + /** + * Returns the last ConferenceInfoDocument sent by us to this + * CallPeer. It is a document with state full + * @return the last ConferenceInfoDocument sent by us to this + * CallPeer. It is a document with state full + */ + public ConferenceInfoDocument getLastConferenceInfoSent() + { + return lastConferenceInfoSent; + } + + /** + * Sets the last ConferenceInfoDocument sent by us to this + * CallPeer. + * @param confInfo the document to set. + */ + public void setLastConferenceInfoSent(ConferenceInfoDocument confInfo) + { + lastConferenceInfoSent = confInfo; + } + + /** + * Gets the time (as obtained by System.currentTimeMillis()) + * at which we last sent a ConferenceInfoDocument to this + * CallPeer. + * @return the time (as obtained by System.currentTimeMillis()) + * at which we last sent a ConferenceInfoDocument to this + * CallPeer. + */ + public long getLastConferenceInfoSentTimestamp() + { + return lastConferenceInfoSentTimestamp; + } + + /** + * Sets the time (as obtained by System.currentTimeMillis()) + * at which we last sent a ConferenceInfoDocument to this + * CallPeer. + * @param newTimestamp the time to set + */ + public void setLastConferenceInfoSentTimestamp(long newTimestamp) + { + lastConferenceInfoSentTimestamp = newTimestamp; + } + + /** + * Gets the last ConferenceInfoDocument sent to us by this + * CallPeer. + * @return the last ConferenceInfoDocument sent to us by this + * CallPeer. + */ + public ConferenceInfoDocument getLastConferenceInfoReceived() + { + return lastConferenceInfoReceived; + } + + /** + * Gets the last ConferenceInfoDocument sent to us by this + * CallPeer. + * @return the last ConferenceInfoDocument sent to us by this + * CallPeer. + */ + public void setLastConferenceInfoReceived(ConferenceInfoDocument confInfo) + { + lastConferenceInfoReceived = confInfo; + } + + /** + * Gets the version of the last ConferenceInfoDocument + * sent to us by this CallPeer, or -1 if we haven't (yet) received + * a ConferenceInformationDocument from this CallPeer. + * @return + */ + public int getLastConferenceInfoReceivedVersion() + { + return (lastConferenceInfoReceived == null) + ? -1 + : lastConferenceInfoReceived.getVersion(); + } + + /** + * Gets the String to be used for this CallPeer when + * we describe it in a ConferenceInfoDocument (e.g. the + * entity key attribute which to use for the user + * element corresponding to this CallPeer) + * + * @return the String to be used for this CallPeer when + * we describe it in a ConferenceInfoDocument (e.g. the + * entity key attribute which to use for the user + * element corresponding to this CallPeer) + */ + 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 fe14a18df..9ecfd1da1 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