public class ShowPreviewDialog extends SIPCommDialog implements ActionListener, ChatLinkClickedListener { /** Serial version UID. */ private static final long serialVersionUID = 1L; /** * The <tt>Logger</tt> used by the <tt>ShowPreviewDialog</tt> class and its instances for logging * output. */ private static final Logger logger = Logger.getLogger(ShowPreviewDialog.class); ConfigurationService cfg = GuiActivator.getConfigurationService(); /** The Ok button. */ private final JButton okButton; /** The cancel button. */ private final JButton cancelButton; /** Checkbox that indicates whether or not to show this dialog next time. */ private final JCheckBox enableReplacementProposal; /** Checkbox that indicates whether or not to show previews automatically */ private final JCheckBox enableReplacement; /** The <tt>ChatConversationPanel</tt> that this dialog is associated with. */ private final ChatConversationPanel chatPanel; /** Mapping between messageID and the string representation of the chat message. */ private Map<String, String> msgIDToChatString = new ConcurrentHashMap<String, String>(); /** * Mapping between the pair (messageID, link position) and the actual link in the string * representation of the chat message. */ private Map<String, String> msgIDandPositionToLink = new ConcurrentHashMap<String, String>(); /** * Mapping between link and replacement for this link that is acquired from it's corresponding * <tt>ReplacementService</tt>. */ private Map<String, String> linkToReplacement = new ConcurrentHashMap<String, String>(); /** The id of the message that is currently associated with this dialog. */ private String currentMessageID = ""; /** The position of the link in the current message. */ private String currentLinkPosition = ""; /** * Creates an instance of <tt>ShowPreviewDialog</tt> * * @param chatPanel The <tt>ChatConversationPanel</tt> that is associated with this dialog. */ ShowPreviewDialog(final ChatConversationPanel chatPanel) { this.chatPanel = chatPanel; this.setTitle( GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW_DIALOG_TITLE")); okButton = new JButton(GuiActivator.getResources().getI18NString("service.gui.OK")); cancelButton = new JButton(GuiActivator.getResources().getI18NString("service.gui.CANCEL")); JPanel mainPanel = new TransparentPanel(); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // mainPanel.setPreferredSize(new Dimension(200, 150)); this.getContentPane().add(mainPanel); JTextPane descriptionMsg = new JTextPane(); descriptionMsg.setEditable(false); descriptionMsg.setOpaque(false); descriptionMsg.setText( GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW_WARNING_DESCRIPTION")); Icon warningIcon = null; try { warningIcon = new ImageIcon( ImageIO.read( GuiActivator.getResources().getImageURL("service.gui.icons.WARNING_ICON"))); } catch (IOException e) { logger.debug("failed to load the warning icon"); } JLabel warningSign = new JLabel(warningIcon); JPanel warningPanel = new TransparentPanel(); warningPanel.setLayout(new BoxLayout(warningPanel, BoxLayout.X_AXIS)); warningPanel.add(warningSign); warningPanel.add(Box.createHorizontalStrut(10)); warningPanel.add(descriptionMsg); enableReplacement = new JCheckBox( GuiActivator.getResources() .getI18NString("plugin.chatconfig.replacement.ENABLE_REPLACEMENT_STATUS")); enableReplacement.setOpaque(false); enableReplacement.setSelected(cfg.getBoolean(ReplacementProperty.REPLACEMENT_ENABLE, true)); enableReplacementProposal = new JCheckBox( GuiActivator.getResources() .getI18NString("plugin.chatconfig.replacement.ENABLE_REPLACEMENT_PROPOSAL")); enableReplacementProposal.setOpaque(false); JPanel checkBoxPanel = new TransparentPanel(); checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.Y_AXIS)); checkBoxPanel.add(enableReplacement); checkBoxPanel.add(enableReplacementProposal); JPanel buttonsPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER)); buttonsPanel.add(okButton); buttonsPanel.add(cancelButton); mainPanel.add(warningPanel); mainPanel.add(Box.createVerticalStrut(10)); mainPanel.add(checkBoxPanel); mainPanel.add(buttonsPanel); okButton.addActionListener(this); cancelButton.addActionListener(this); this.setPreferredSize(new Dimension(390, 230)); } @Override public void actionPerformed(ActionEvent arg0) { if (arg0.getSource().equals(okButton)) { cfg.setProperty(ReplacementProperty.REPLACEMENT_ENABLE, enableReplacement.isSelected()); cfg.setProperty( ReplacementProperty.REPLACEMENT_PROPOSAL, enableReplacementProposal.isSelected()); SwingWorker worker = new SwingWorker() { /** * Called on the event dispatching thread (not on the worker thread) after the <code> * construct</code> method has returned. */ @Override public void finished() { String newChatString = (String) get(); if (newChatString != null) { try { Element elem = chatPanel.document.getElement(currentMessageID); chatPanel.document.setOuterHTML(elem, newChatString); msgIDToChatString.put(currentMessageID, newChatString); } catch (BadLocationException ex) { logger.error("Could not replace chat message", ex); } catch (IOException ex) { logger.error("Could not replace chat message", ex); } } } @Override protected Object construct() throws Exception { String newChatString = msgIDToChatString.get(currentMessageID); try { String originalLink = msgIDandPositionToLink.get(currentMessageID + "#" + currentLinkPosition); String replacementLink = linkToReplacement.get(originalLink); String replacement; DirectImageReplacementService source = GuiActivator.getDirectImageReplacementSource(); if (originalLink.equals(replacementLink) && (!source.isDirectImage(originalLink) || source.getImageSize(originalLink) == -1)) { replacement = originalLink; } else { replacement = "<IMG HEIGHT=\"90\" WIDTH=\"120\" SRC=\"" + replacementLink + "\" BORDER=\"0\" ALT=\"" + originalLink + "\"></IMG>"; } String old = originalLink + "</A> <A href=\"jitsi://" + ShowPreviewDialog.this.getClass().getName() + "/SHOWPREVIEW?" + currentMessageID + "#" + currentLinkPosition + "\">" + GuiActivator.getResources().getI18NString("service.gui.SHOW_PREVIEW"); newChatString = newChatString.replace(old, replacement); } catch (Exception ex) { logger.error("Could not replace chat message", ex); } return newChatString; } }; worker.start(); this.setVisible(false); } else if (arg0.getSource().equals(cancelButton)) { this.setVisible(false); } } @Override public void chatLinkClicked(URI url) { String action = url.getPath(); if (action.equals("/SHOWPREVIEW")) { enableReplacement.setSelected(cfg.getBoolean(ReplacementProperty.REPLACEMENT_ENABLE, true)); enableReplacementProposal.setSelected( cfg.getBoolean(ReplacementProperty.REPLACEMENT_PROPOSAL, true)); currentMessageID = url.getQuery(); currentLinkPosition = url.getFragment(); this.setVisible(true); this.setLocationRelativeTo(chatPanel); } } /** * Returns mapping between messageID and the string representation of the chat message. * * @return mapping between messageID and chat string. */ Map<String, String> getMsgIDToChatString() { return msgIDToChatString; } /** * Returns mapping between the pair (messageID, link position) and the actual link in the string * representation of the chat message. * * @return mapping between (messageID, linkPosition) and link. */ Map<String, String> getMsgIDandPositionToLink() { return msgIDandPositionToLink; } /** * Returns mapping between link and replacement for this link that was acquired from it's * corresponding <tt>ReplacementService</tt>. * * @return mapping between link and it's corresponding replacement. */ Map<String, String> getLinkToReplacement() { return linkToReplacement; } }
/** * Class is a transport layer for WebRTC data channels. It consists of SCTP connection running on * top of ICE/DTLS layer. Manages WebRTC data channels. See * http://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-08 for more info on WebRTC data * channels. * * <p>Control protocol: http://tools.ietf.org/html/draft-ietf-rtcweb-data-protocol-03 FIXME handle * closing of data channels(SCTP stream reset) * * @author Pawel Domas * @author Lyubomir Marinov * @author Boris Grozev */ public class SctpConnection extends Channel implements SctpDataCallback, SctpSocket.NotificationListener { /** Generator used to track debug IDs. */ private static int debugIdGen = -1; /** DTLS transport buffer size. Note: randomly chosen. */ private static final int DTLS_BUFFER_SIZE = 2048; /** Switch used for debugging SCTP traffic purposes. FIXME to be removed */ private static final boolean LOG_SCTP_PACKETS = false; /** The logger */ private static final Logger logger = Logger.getLogger(SctpConnection.class); /** * Message type used to acknowledge WebRTC data channel allocation on SCTP stream ID on which * <tt>MSG_OPEN_CHANNEL</tt> message arrives. */ private static final int MSG_CHANNEL_ACK = 0x2; private static final byte[] MSG_CHANNEL_ACK_BYTES = new byte[] {MSG_CHANNEL_ACK}; /** * Message with this type sent over control PPID in order to open new WebRTC data channel on SCTP * stream ID that this message is sent. */ private static final int MSG_OPEN_CHANNEL = 0x3; /** SCTP transport buffer size. */ private static final int SCTP_BUFFER_SIZE = DTLS_BUFFER_SIZE - 13; /** The pool of <tt>Thread</tt>s which run <tt>SctpConnection</tt>s. */ private static final ExecutorService threadPool = ExecutorUtils.newCachedThreadPool(true, SctpConnection.class.getName()); /** Payload protocol id that identifies binary data in WebRTC data channel. */ static final int WEB_RTC_PPID_BIN = 53; /** Payload protocol id for control data. Used for <tt>WebRtcDataStream</tt> allocation. */ static final int WEB_RTC_PPID_CTRL = 50; /** Payload protocol id that identifies text data UTF8 encoded in WebRTC data channels. */ static final int WEB_RTC_PPID_STRING = 51; /** * The <tt>String</tt> value of the <tt>Protocol</tt> field of the <tt>DATA_CHANNEL_OPEN</tt> * message. */ private static final String WEBRTC_DATA_CHANNEL_PROTOCOL = "http://jitsi.org/protocols/colibri"; private static synchronized int generateDebugId() { debugIdGen += 2; return debugIdGen; } /** * Indicates whether the STCP association is ready and has not been ended by a subsequent state * change. */ private boolean assocIsUp; /** Indicates if we have accepted incoming connection. */ private boolean acceptedIncomingConnection; /** Data channels mapped by SCTP stream identified(sid). */ private final Map<Integer, WebRtcDataStream> channels = new HashMap<Integer, WebRtcDataStream>(); /** Debug ID used to distinguish SCTP sockets in packet logs. */ private final int debugId; /** * The <tt>AsyncExecutor</tt> which is to asynchronously dispatch the events fired by this * instance in order to prevent possible listeners from blocking this <tt>SctpConnection</tt> in * general and {@link #sctpSocket} in particular for too long. The timeout of <tt>15</tt> is * chosen to be in accord with the time it takes to expire a <tt>Channel</tt>. */ private final AsyncExecutor<Runnable> eventDispatcher = new AsyncExecutor<Runnable>(15, TimeUnit.MILLISECONDS); /** Datagram socket for ICE/UDP layer. */ private IceSocketWrapper iceSocket; /** * List of <tt>WebRtcDataStreamListener</tt>s that will be notified whenever new WebRTC data * channel is opened. */ private final List<WebRtcDataStreamListener> listeners = new ArrayList<WebRtcDataStreamListener>(); /** Remote SCTP port. */ private final int remoteSctpPort; /** <tt>SctpSocket</tt> used for SCTP transport. */ private SctpSocket sctpSocket; /** * Flag prevents from starting this connection multiple times from {@link #maybeStartStream()}. */ private boolean started; /** * Initializes a new <tt>SctpConnection</tt> instance. * * @param id the string identifier of this connection instance * @param content the <tt>Content</tt> which is initializing the new instance * @param endpoint the <tt>Endpoint</tt> of newly created instance * @param remoteSctpPort the SCTP port used by remote peer * @param channelBundleId the ID of the channel-bundle this <tt>SctpConnection</tt> is to be a * part of (or <tt>null</tt> if no it is not to be a part of a channel-bundle). * @throws Exception if an error occurs while initializing the new instance */ public SctpConnection( String id, Content content, Endpoint endpoint, int remoteSctpPort, String channelBundleId) throws Exception { super(content, id, channelBundleId); setEndpoint(endpoint.getID()); this.remoteSctpPort = remoteSctpPort; this.debugId = generateDebugId(); } /** * Adds <tt>WebRtcDataStreamListener</tt> to the list of listeners. * * @param listener the <tt>WebRtcDataStreamListener</tt> to be added to the listeners list. */ public void addChannelListener(WebRtcDataStreamListener listener) { if (listener == null) { throw new NullPointerException("listener"); } else { synchronized (listeners) { if (!listeners.contains(listener)) { listeners.add(listener); } } } } /** {@inheritDoc} */ @Override protected void closeStream() throws IOException { try { synchronized (this) { assocIsUp = false; acceptedIncomingConnection = false; if (sctpSocket != null) { sctpSocket.close(); sctpSocket = null; } } } finally { if (iceSocket != null) { // It is now the responsibility of the transport manager to // close the socket. // iceUdpSocket.close(); } } } /** {@inheritDoc} */ @Override public void expire() { try { eventDispatcher.shutdown(); } finally { super.expire(); } } /** * Gets the <tt>WebRtcDataStreamListener</tt>s added to this instance. * * @return the <tt>WebRtcDataStreamListener</tt>s added to this instance or <tt>null</tt> if there * are no <tt>WebRtcDataStreamListener</tt>s added to this instance */ private WebRtcDataStreamListener[] getChannelListeners() { WebRtcDataStreamListener[] ls; synchronized (listeners) { if (listeners.isEmpty()) { ls = null; } else { ls = listeners.toArray(new WebRtcDataStreamListener[listeners.size()]); } } return ls; } /** * Returns default <tt>WebRtcDataStream</tt> if it's ready or <tt>null</tt> otherwise. * * @return <tt>WebRtcDataStream</tt> if it's ready or <tt>null</tt> otherwise. * @throws IOException */ public WebRtcDataStream getDefaultDataStream() throws IOException { WebRtcDataStream def; synchronized (this) { if (sctpSocket == null) { def = null; } else { // Channel that runs on sid 0 def = channels.get(0); if (def == null) { def = openChannel(0, 0, 0, 0, "default"); } // Pawel Domas: Must be acknowledged before use /* * XXX Lyubomir Marinov: We're always sending ordered. According * to "WebRTC Data Channel Establishment Protocol", we can start * sending messages containing user data after the * DATA_CHANNEL_OPEN message has been sent without waiting for * the reception of the corresponding DATA_CHANNEL_ACK message. */ // if (!def.isAcknowledged()) // def = null; } } return def; } /** * Returns <tt>true</tt> if this <tt>SctpConnection</tt> is connected to the remote peer and * operational. * * @return <tt>true</tt> if this <tt>SctpConnection</tt> is connected to the remote peer and * operational */ public boolean isReady() { return assocIsUp && acceptedIncomingConnection; } /** {@inheritDoc} */ @Override protected void maybeStartStream() throws IOException { // connector final StreamConnector connector = getStreamConnector(); if (connector == null) return; synchronized (this) { if (started) return; threadPool.execute( new Runnable() { @Override public void run() { try { Sctp.init(); runOnDtlsTransport(connector); } catch (IOException e) { logger.error(e, e); } finally { try { Sctp.finish(); } catch (IOException e) { logger.error("Failed to shutdown SCTP stack", e); } } } }); started = true; } } /** * Submits {@link #notifyChannelOpenedInEventDispatcher(WebRtcDataStream)} to {@link * #eventDispatcher} for asynchronous execution. * * @param dataChannel */ private void notifyChannelOpened(final WebRtcDataStream dataChannel) { if (!isExpired()) { eventDispatcher.execute( new Runnable() { @Override public void run() { notifyChannelOpenedInEventDispatcher(dataChannel); } }); } } private void notifyChannelOpenedInEventDispatcher(WebRtcDataStream dataChannel) { /* * When executing asynchronously in eventDispatcher, it is technically * possible that this SctpConnection may have expired by now. */ if (!isExpired()) { WebRtcDataStreamListener[] ls = getChannelListeners(); if (ls != null) { for (WebRtcDataStreamListener l : ls) { l.onChannelOpened(this, dataChannel); } } } } /** * Submits {@link #notifySctpConnectionReadyInEventDispatcher()} to {@link #eventDispatcher} for * asynchronous execution. */ private void notifySctpConnectionReady() { if (!isExpired()) { eventDispatcher.execute( new Runnable() { @Override public void run() { notifySctpConnectionReadyInEventDispatcher(); } }); } } /** * Notifies the <tt>WebRtcDataStreamListener</tt>s added to this instance that this * <tt>SctpConnection</tt> is ready i.e. it is connected to the remote peer and operational. */ private void notifySctpConnectionReadyInEventDispatcher() { /* * When executing asynchronously in eventDispatcher, it is technically * possible that this SctpConnection may have expired by now. */ if (!isExpired() && isReady()) { WebRtcDataStreamListener[] ls = getChannelListeners(); if (ls != null) { for (WebRtcDataStreamListener l : ls) { l.onSctpConnectionReady(this); } } } } /** * Handles control packet. * * @param data raw packet data that arrived on control PPID. * @param sid SCTP stream id on which the data has arrived. */ private synchronized void onCtrlPacket(byte[] data, int sid) throws IOException { ByteBuffer buffer = ByteBuffer.wrap(data); int messageType = /* 1 byte unsigned integer */ 0xFF & buffer.get(); if (messageType == MSG_CHANNEL_ACK) { if (logger.isDebugEnabled()) { logger.debug(getEndpoint().getID() + " ACK received SID: " + sid); } // Open channel ACK WebRtcDataStream channel = channels.get(sid); if (channel != null) { // Ack check prevents from firing multiple notifications // if we get more than one ACKs (by mistake/bug). if (!channel.isAcknowledged()) { channel.ackReceived(); notifyChannelOpened(channel); } else { logger.warn("Redundant ACK received for SID: " + sid); } } else { logger.error("No channel exists on sid: " + sid); } } else if (messageType == MSG_OPEN_CHANNEL) { int channelType = /* 1 byte unsigned integer */ 0xFF & buffer.get(); int priority = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort(); long reliability = /* 4 bytes unsigned integer */ 0xFFFFFFFFL & buffer.getInt(); int labelLength = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort(); int protocolLength = /* 2 bytes unsigned integer */ 0xFFFF & buffer.getShort(); String label; String protocol; if (labelLength == 0) { label = ""; } else { byte[] labelBytes = new byte[labelLength]; buffer.get(labelBytes); label = new String(labelBytes, "UTF-8"); } if (protocolLength == 0) { protocol = ""; } else { byte[] protocolBytes = new byte[protocolLength]; buffer.get(protocolBytes); protocol = new String(protocolBytes, "UTF-8"); } if (logger.isDebugEnabled()) { logger.debug( "!!! " + getEndpoint().getID() + " data channel open request on SID: " + sid + " type: " + channelType + " prio: " + priority + " reliab: " + reliability + " label: " + label + " proto: " + protocol); } if (channels.containsKey(sid)) { logger.error("Channel on sid: " + sid + " already exists"); } WebRtcDataStream newChannel = new WebRtcDataStream(sctpSocket, sid, label, true); channels.put(sid, newChannel); sendOpenChannelAck(sid); notifyChannelOpened(newChannel); } else { logger.error("Unexpected ctrl msg type: " + messageType); } } /** {@inheritDoc} */ @Override protected void onEndpointChanged(Endpoint oldValue, Endpoint newValue) { if (oldValue != null) oldValue.setSctpConnection(null); if (newValue != null) newValue.setSctpConnection(this); } /** Implements notification in order to track socket state. */ @Override public synchronized void onSctpNotification(SctpSocket socket, SctpNotification notification) { if (logger.isDebugEnabled()) { logger.debug("socket=" + socket + "; notification=" + notification); } switch (notification.sn_type) { case SctpNotification.SCTP_ASSOC_CHANGE: SctpNotification.AssociationChange assocChange = (SctpNotification.AssociationChange) notification; switch (assocChange.state) { case SctpNotification.AssociationChange.SCTP_COMM_UP: if (!assocIsUp) { boolean wasReady = isReady(); assocIsUp = true; if (isReady() && !wasReady) notifySctpConnectionReady(); } break; case SctpNotification.AssociationChange.SCTP_COMM_LOST: case SctpNotification.AssociationChange.SCTP_SHUTDOWN_COMP: case SctpNotification.AssociationChange.SCTP_CANT_STR_ASSOC: try { closeStream(); } catch (IOException e) { logger.error("Error closing SCTP socket", e); } break; } break; } } /** * {@inheritDoc} * * <p>SCTP input data callback. */ @Override public void onSctpPacket( byte[] data, int sid, int ssn, int tsn, long ppid, int context, int flags) { if (ppid == WEB_RTC_PPID_CTRL) { // Channel control PPID try { onCtrlPacket(data, sid); } catch (IOException e) { logger.error("IOException when processing ctrl packet", e); } } else if (ppid == WEB_RTC_PPID_STRING || ppid == WEB_RTC_PPID_BIN) { WebRtcDataStream channel; synchronized (this) { channel = channels.get(sid); } if (channel == null) { logger.error("No channel found for sid: " + sid); return; } if (ppid == WEB_RTC_PPID_STRING) { // WebRTC String String str; String charsetName = "UTF-8"; try { str = new String(data, charsetName); } catch (UnsupportedEncodingException uee) { logger.error("Unsupported charset encoding/name " + charsetName, uee); str = null; } channel.onStringMsg(str); } else { // WebRTC Binary channel.onBinaryMsg(data); } } else { logger.warn("Got message on unsupported PPID: " + ppid); } } /** * Opens new WebRTC data channel using specified parameters. * * @param type channel type as defined in control protocol description. Use 0 for "reliable". * @param prio channel priority. The higher the number, the lower the priority. * @param reliab Reliability Parameter<br> * This field is ignored if a reliable channel is used. If a partial reliable channel with * limited number of retransmissions is used, this field specifies the number of * retransmissions. If a partial reliable channel with limited lifetime is used, this field * specifies the maximum lifetime in milliseconds. The following table summarizes this:<br> * </br> * <p>+------------------------------------------------+------------------+ | Channel Type | * Reliability | | | Parameter | * +------------------------------------------------+------------------+ | * DATA_CHANNEL_RELIABLE | Ignored | | DATA_CHANNEL_RELIABLE_UNORDERED | Ignored | | * DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT | Number of RTX | | * DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED | Number of RTX | | * DATA_CHANNEL_PARTIAL_RELIABLE_TIMED | Lifetime in ms | | * DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED | Lifetime in ms | * +------------------------------------------------+------------------+ * @param sid SCTP stream id that will be used by new channel (it must not be already used). * @param label text label for the channel. * @return new instance of <tt>WebRtcDataStream</tt> that represents opened WebRTC data channel. * @throws IOException if IO error occurs. */ public synchronized WebRtcDataStream openChannel( int type, int prio, long reliab, int sid, String label) throws IOException { if (channels.containsKey(sid)) { throw new IOException("Channel on sid: " + sid + " already exists"); } // Label Length & Label byte[] labelBytes; int labelByteLength; if (label == null) { labelBytes = null; labelByteLength = 0; } else { labelBytes = label.getBytes("UTF-8"); labelByteLength = labelBytes.length; if (labelByteLength > 0xFFFF) labelByteLength = 0xFFFF; } // Protocol Length & Protocol String protocol = WEBRTC_DATA_CHANNEL_PROTOCOL; byte[] protocolBytes; int protocolByteLength; if (protocol == null) { protocolBytes = null; protocolByteLength = 0; } else { protocolBytes = protocol.getBytes("UTF-8"); protocolByteLength = protocolBytes.length; if (protocolByteLength > 0xFFFF) protocolByteLength = 0xFFFF; } ByteBuffer packet = ByteBuffer.allocate(12 + labelByteLength + protocolByteLength); // Message open new channel on current sid // Message Type packet.put((byte) MSG_OPEN_CHANNEL); // Channel Type packet.put((byte) type); // Priority packet.putShort((short) prio); // Reliability Parameter packet.putInt((int) reliab); // Label Length packet.putShort((short) labelByteLength); // Protocol Length packet.putShort((short) protocolByteLength); // Label if (labelByteLength != 0) { packet.put(labelBytes, 0, labelByteLength); } // Protocol if (protocolByteLength != 0) { packet.put(protocolBytes, 0, protocolByteLength); } int sentCount = sctpSocket.send(packet.array(), true, sid, WEB_RTC_PPID_CTRL); if (sentCount != packet.capacity()) { throw new IOException("Failed to open new chanel on sid: " + sid); } WebRtcDataStream channel = new WebRtcDataStream(sctpSocket, sid, label, false); channels.put(sid, channel); return channel; } /** * Removes <tt>WebRtcDataStreamListener</tt> from the list of listeners. * * @param listener the <tt>WebRtcDataStreamListener</tt> to be removed from the listeners list. */ public void removeChannelListener(WebRtcDataStreamListener listener) { if (listener != null) { synchronized (listeners) { listeners.remove(listener); } } } private void runOnDtlsTransport(StreamConnector connector) throws IOException { DtlsControlImpl dtlsControl = (DtlsControlImpl) getTransportManager().getDtlsControl(this); DtlsTransformEngine engine = dtlsControl.getTransformEngine(); final DtlsPacketTransformer transformer = (DtlsPacketTransformer) engine.getRTPTransformer(); byte[] receiveBuffer = new byte[SCTP_BUFFER_SIZE]; if (LOG_SCTP_PACKETS) { System.setProperty( ConfigurationService.PNAME_SC_HOME_DIR_LOCATION, System.getProperty("java.io.tmpdir")); System.setProperty( ConfigurationService.PNAME_SC_HOME_DIR_NAME, SctpConnection.class.getName()); } synchronized (this) { // FIXME local SCTP port is hardcoded in bridge offer SDP (Jitsi // Meet) sctpSocket = Sctp.createSocket(5000); assocIsUp = false; acceptedIncomingConnection = false; } // Implement output network link for SCTP stack on DTLS transport sctpSocket.setLink( new NetworkLink() { @Override public void onConnOut(SctpSocket s, byte[] packet) throws IOException { if (LOG_SCTP_PACKETS) { LibJitsi.getPacketLoggingService() .logPacket( PacketLoggingService.ProtocolName.ICE4J, new byte[] {0, 0, 0, (byte) debugId}, 5000, new byte[] {0, 0, 0, (byte) (debugId + 1)}, remoteSctpPort, PacketLoggingService.TransportName.UDP, true, packet); } // Send through DTLS transport transformer.sendApplicationData(packet, 0, packet.length); } }); if (logger.isDebugEnabled()) { logger.debug("Connecting SCTP to port: " + remoteSctpPort + " to " + getEndpoint().getID()); } sctpSocket.setNotificationListener(this); sctpSocket.listen(); // FIXME manage threads threadPool.execute( new Runnable() { @Override public void run() { SctpSocket sctpSocket = null; try { // sctpSocket is set to null on close sctpSocket = SctpConnection.this.sctpSocket; while (sctpSocket != null) { if (sctpSocket.accept()) { acceptedIncomingConnection = true; break; } Thread.sleep(100); sctpSocket = SctpConnection.this.sctpSocket; } if (isReady()) { notifySctpConnectionReady(); } } catch (Exception e) { logger.error("Error accepting SCTP connection", e); } if (sctpSocket == null && logger.isInfoEnabled()) { logger.info( "SctpConnection " + getID() + " closed" + " before SctpSocket accept()-ed."); } } }); // Notify that from now on SCTP connection is considered functional sctpSocket.setDataCallback(this); // Setup iceSocket DatagramSocket datagramSocket = connector.getDataSocket(); if (datagramSocket != null) { this.iceSocket = new IceUdpSocketWrapper(datagramSocket); } else { this.iceSocket = new IceTcpSocketWrapper(connector.getDataTCPSocket()); } DatagramPacket rcvPacket = new DatagramPacket(receiveBuffer, 0, receiveBuffer.length); // Receive loop, breaks when SCTP socket is closed try { do { iceSocket.receive(rcvPacket); RawPacket raw = new RawPacket(rcvPacket.getData(), rcvPacket.getOffset(), rcvPacket.getLength()); raw = transformer.reverseTransform(raw); // Check for app data if (raw == null) continue; if (LOG_SCTP_PACKETS) { LibJitsi.getPacketLoggingService() .logPacket( PacketLoggingService.ProtocolName.ICE4J, new byte[] {0, 0, 0, (byte) (debugId + 1)}, remoteSctpPort, new byte[] {0, 0, 0, (byte) debugId}, 5000, PacketLoggingService.TransportName.UDP, false, raw.getBuffer(), raw.getOffset(), raw.getLength()); } // Pass network packet to SCTP stack sctpSocket.onConnIn(raw.getBuffer(), raw.getOffset(), raw.getLength()); } while (true); } finally { // Eventually, close the socket although it should happen from // expire(). synchronized (this) { assocIsUp = false; acceptedIncomingConnection = false; if (sctpSocket != null) { sctpSocket.close(); sctpSocket = null; } } } } /** * Sends acknowledgment for open channel request on given SCTP stream ID. * * @param sid SCTP stream identifier to be used for sending ack. */ private void sendOpenChannelAck(int sid) throws IOException { // Send ACK byte[] ack = MSG_CHANNEL_ACK_BYTES; int sendAck = sctpSocket.send(ack, true, sid, WEB_RTC_PPID_CTRL); if (sendAck != ack.length) { logger.error("Failed to send open channel confirmation"); } } /** * {@inheritDoc} * * <p>Creates a <tt>TransportManager</tt> instance suitable for an <tt>SctpConnection</tt> (e.g. * with 1 component only). */ protected TransportManager createTransportManager(String xmlNamespace) throws IOException { if (IceUdpTransportPacketExtension.NAMESPACE.equals(xmlNamespace)) { Content content = getContent(); return new IceUdpTransportManager( content.getConference(), isInitiator(), 1 /* num components */, content.getName()); } else if (RawUdpTransportPacketExtension.NAMESPACE.equals(xmlNamespace)) { // TODO: support RawUdp once RawUdpTransportManager is updated // return new RawUdpTransportManager(this); throw new IllegalArgumentException("Unsupported Jingle transport " + xmlNamespace); } else { throw new IllegalArgumentException("Unsupported Jingle transport " + xmlNamespace); } } }
/** * A depacketizer from VP8. See {@link "http://tools.ietf.org/html/draft-ietf-payload-vp8-11"} * * @author Boris Grozev * @author George Politis */ public class DePacketizer extends AbstractCodec2 { /** * The <tt>Logger</tt> used by the <tt>DePacketizer</tt> class and its instances for logging * output. */ private static final Logger logger = Logger.getLogger(DePacketizer.class); /** Whether trace logging is enabled. */ private static final boolean TRACE = logger.isTraceEnabled(); /** * A <tt>Comparator</tt> implementation for RTP sequence numbers. Compares <tt>a</tt> and * <tt>b</tt>, taking into account the wrap at 2^16. * * <p>IMPORTANT: This is a valid <tt>Comparator</tt> implementation only if used for subsets of * [0, 2^16) which don't span more than 2^15 elements. * * <p>E.g. it works for: [0, 2^15-1] and ([50000, 2^16) u [0, 10000]) Doesn't work for: [0, 2^15] * and ([0, 2^15-1] u {2^16-1}) and [0, 2^16) * * <p>NOTE: An identical implementation for Integers can be found in the class SeqNumComparator. * Sequence numbers are 16 bits and unsigned, so an Integer should be sufficient to hold that. */ private static final Comparator<? super Long> seqNumComparator = new Comparator<Long>() { @Override public int compare(Long a, Long b) { if (a.equals(b)) return 0; else if (a > b) { if (a - b < 32768) return 1; else return -1; } else // a < b { if (b - a < 32768) return -1; else return 1; } } }; /** * Stores the RTP payloads (VP8 payload descriptor stripped) from RTP packets belonging to a * single VP8 compressed frame. */ private SortedMap<Long, Container> data = new TreeMap<Long, Container>(seqNumComparator); /** Stores unused <tt>Container</tt>'s. */ private Queue<Container> free = new ArrayBlockingQueue<Container>(100); /** * Stores the first (earliest) sequence number stored in <tt>data</tt>, or -1 if <tt>data</tt> is * empty. */ private long firstSeq = -1; /** * Stores the last (latest) sequence number stored in <tt>data</tt>, or -1 if <tt>data</tt> is * empty. */ private long lastSeq = -1; /** * Stores the value of the <tt>PictureID</tt> field for the VP8 compressed frame, parts of which * are currently stored in <tt>data</tt>, or -1 if the <tt>PictureID</tt> field is not in use or * <tt>data</tt> is empty. */ private int pictureId = -1; /** * Stores the RTP timestamp of the packets stored in <tt>data</tt>, or -1 if they don't have a * timestamp set. */ private long timestamp = -1; /** Whether we have stored any packets in <tt>data</tt>. Equivalent to <tt>data.isEmpty()</tt>. */ private boolean empty = true; /** * Whether we have stored in <tt>data</tt> the last RTP packet of the VP8 compressed frame, parts * of which are currently stored in <tt>data</tt>. */ private boolean haveEnd = false; /** * Whether we have stored in <tt>data</tt> the first RTP packet of the VP8 compressed frame, parts * of which are currently stored in <tt>data</tt>. */ private boolean haveStart = false; /** * Stores the sum of the lengths of the data stored in <tt>data</tt>, that is the total length of * the VP8 compressed frame to be constructed. */ private int frameLength = 0; /** The sequence number of the last RTP packet, which was included in the output. */ private long lastSentSeq = -1; /** Initializes a new <tt>JNIEncoder</tt> instance. */ public DePacketizer() { super( "VP8 RTP DePacketizer", VideoFormat.class, new VideoFormat[] {new VideoFormat(Constants.VP8)}); inputFormats = new VideoFormat[] {new VideoFormat(Constants.VP8_RTP)}; } /** {@inheritDoc} */ @Override protected void doClose() {} /** {@inheritDoc} */ @Override protected void doOpen() throws ResourceUnavailableException { if (logger.isInfoEnabled()) logger.info("Opened VP8 depacketizer"); } /** * Re-initializes the fields which store information about the currently held data. Empties * <tt>data</tt>. */ private void reinit() { firstSeq = lastSeq = timestamp = -1; pictureId = -1; empty = true; haveEnd = haveStart = false; frameLength = 0; Iterator<Map.Entry<Long, Container>> it = data.entrySet().iterator(); Map.Entry<Long, Container> e; while (it.hasNext()) { e = it.next(); free.offer(e.getValue()); it.remove(); } } /** * Checks whether the currently held VP8 compressed frame is complete (e.g all its packets are * stored in <tt>data</tt>). * * @return <tt>true</tt> if the currently help VP8 compressed frame is complete, <tt>false</tt> * otherwise. */ private boolean frameComplete() { return haveStart && haveEnd && !haveMissing(); } /** * Checks whether there are packets with sequence numbers between <tt>firstSeq</tt> and * <tt>lastSeq</tt> which are *not* stored in <tt>data</tt>. * * @return <tt>true</tt> if there are packets with sequence numbers between <tt>firstSeq</tt> and * <tt>lastSeq</tt> which are *not* stored in <tt>data</tt>. */ private boolean haveMissing() { Set<Long> seqs = data.keySet(); long s = firstSeq; while (s != lastSeq) { if (!seqs.contains(s)) return true; s = (s + 1) % (1 << 16); } return false; } /** {@inheritDoc} */ @Override protected int doProcess(Buffer inBuffer, Buffer outBuffer) { byte[] inData = (byte[]) inBuffer.getData(); int inOffset = inBuffer.getOffset(); if (!VP8PayloadDescriptor.isValid(inData, inOffset)) { logger.warn("Invalid RTP/VP8 packet discarded."); outBuffer.setDiscard(true); return BUFFER_PROCESSED_FAILED; // XXX: FAILED or OK? } long inSeq = inBuffer.getSequenceNumber(); long inRtpTimestamp = inBuffer.getRtpTimeStamp(); int inPictureId = VP8PayloadDescriptor.getPictureId(inData, inOffset); boolean inMarker = (inBuffer.getFlags() & Buffer.FLAG_RTP_MARKER) != 0; boolean inIsStartOfFrame = VP8PayloadDescriptor.isStartOfFrame(inData, inOffset); int inLength = inBuffer.getLength(); int inPdSize = VP8PayloadDescriptor.getSize(inData, inOffset); int inPayloadLength = inLength - inPdSize; if (empty && lastSentSeq != -1 && seqNumComparator.compare(inSeq, lastSentSeq) != 1) { if (logger.isInfoEnabled()) logger.info("Discarding old packet (while empty) " + inSeq); outBuffer.setDiscard(true); return BUFFER_PROCESSED_OK; } if (!empty) { // if the incoming packet has a different PictureID or timestamp // than those of the current frame, then it belongs to a different // frame. if ((inPictureId != -1 && pictureId != -1 && inPictureId != pictureId) | (timestamp != -1 && inRtpTimestamp != -1 && inRtpTimestamp != timestamp)) { if (seqNumComparator.compare(inSeq, firstSeq) != 1) // inSeq <= firstSeq { // the packet belongs to a previous frame. discard it if (logger.isInfoEnabled()) logger.info("Discarding old packet " + inSeq); outBuffer.setDiscard(true); return BUFFER_PROCESSED_OK; } else // inSeq > firstSeq (and also presumably isSeq > lastSeq) { // the packet belongs to a subsequent frame (to the one // currently being held). Drop the current frame. if (logger.isInfoEnabled()) logger.info( "Discarding saved packets on arrival of" + " a packet for a subsequent frame: " + inSeq); // TODO: this would be the place to complain about the // not-well-received PictureID by sending a RTCP SLI or NACK. reinit(); } } } // a whole frame in a single packet. avoid the extra copy to // this.data and output it immediately. if (empty && inMarker && inIsStartOfFrame) { byte[] outData = validateByteArraySize(outBuffer, inPayloadLength, false); System.arraycopy(inData, inOffset + inPdSize, outData, 0, inPayloadLength); outBuffer.setOffset(0); outBuffer.setLength(inPayloadLength); outBuffer.setRtpTimeStamp(inBuffer.getRtpTimeStamp()); if (TRACE) logger.trace("Out PictureID=" + inPictureId); lastSentSeq = inSeq; return BUFFER_PROCESSED_OK; } // add to this.data Container container = free.poll(); if (container == null) container = new Container(); if (container.buf == null || container.buf.length < inPayloadLength) container.buf = new byte[inPayloadLength]; if (data.get(inSeq) != null) { if (logger.isInfoEnabled()) logger.info("(Probable) duplicate packet detected, discarding " + inSeq); outBuffer.setDiscard(true); return BUFFER_PROCESSED_OK; } System.arraycopy(inData, inOffset + inPdSize, container.buf, 0, inPayloadLength); container.len = inPayloadLength; data.put(inSeq, container); // update fields frameLength += inPayloadLength; if (firstSeq == -1 || (seqNumComparator.compare(firstSeq, inSeq) == 1)) firstSeq = inSeq; if (lastSeq == -1 || (seqNumComparator.compare(inSeq, lastSeq) == 1)) lastSeq = inSeq; if (empty) { // the first received packet for the current frame was just added empty = false; timestamp = inRtpTimestamp; pictureId = inPictureId; } if (inMarker) haveEnd = true; if (inIsStartOfFrame) haveStart = true; // check if we have a full frame if (frameComplete()) { byte[] outData = validateByteArraySize(outBuffer, frameLength, false); int ptr = 0; Container b; for (Map.Entry<Long, Container> entry : data.entrySet()) { b = entry.getValue(); System.arraycopy(b.buf, 0, outData, ptr, b.len); ptr += b.len; } outBuffer.setOffset(0); outBuffer.setLength(frameLength); outBuffer.setRtpTimeStamp(inBuffer.getRtpTimeStamp()); if (TRACE) logger.trace("Out PictureID=" + inPictureId); lastSentSeq = lastSeq; // prepare for the next frame reinit(); return BUFFER_PROCESSED_OK; } else { // frame not complete yet outBuffer.setDiscard(true); return OUTPUT_BUFFER_NOT_FILLED; } } /** * Returns true if the buffer contains a VP8 key frame at offset <tt>offset</tt>. * * @param buff the byte buffer to check * @param off the offset in the byte buffer where the actual data starts * @param len the length of the data in the byte buffer * @return true if the buffer contains a VP8 key frame at offset <tt>offset</tt>. */ public static boolean isKeyFrame(byte[] buff, int off, int len) { if (buff == null || buff.length < off + len || len < RawPacket.FIXED_HEADER_SIZE) { return false; } // Check if this is the start of a VP8 partition in the payload // descriptor. if (!DePacketizer.VP8PayloadDescriptor.isValid(buff, off)) { return false; } if (!DePacketizer.VP8PayloadDescriptor.isStartOfFrame(buff, off)) { return false; } int szVP8PayloadDescriptor = DePacketizer.VP8PayloadDescriptor.getSize(buff, off); return DePacketizer.VP8PayloadHeader.isKeyFrame(buff, off + szVP8PayloadDescriptor); } /** * A class that represents the VP8 Payload Descriptor structure defined in {@link * "http://tools.ietf.org/html/draft-ietf-payload-vp8-10"} */ public static class VP8PayloadDescriptor { /** I bit from the X byte of the Payload Descriptor. */ private static final byte I_BIT = (byte) 0x80; /** K bit from the X byte of the Payload Descriptor. */ private static final byte K_BIT = (byte) 0x10; /** L bit from the X byte of the Payload Descriptor. */ private static final byte L_BIT = (byte) 0x40; /** I bit from the I byte of the Payload Descriptor. */ private static final byte M_BIT = (byte) 0x80; /** Maximum length of a VP8 Payload Descriptor. */ public static final int MAX_LENGTH = 6; /** S bit from the first byte of the Payload Descriptor. */ private static final byte S_BIT = (byte) 0x10; /** T bit from the X byte of the Payload Descriptor. */ private static final byte T_BIT = (byte) 0x20; /** X bit from the first byte of the Payload Descriptor. */ private static final byte X_BIT = (byte) 0x80; /** * Gets the temporal layer index (TID), if that's set. * * @param buf the byte buffer that holds the VP8 packet. * @param off the offset in the byte buffer where the VP8 packet starts. * @param len the length of the VP8 packet. * @return the temporal layer index (TID), if that's set, -1 otherwise. */ public static int getTemporalLayerIndex(byte[] buf, int off, int len) { if (buf == null || buf.length < off + len || len < 2) { return -1; } if ((buf[off] & X_BIT) == 0 || (buf[off + 1] & T_BIT) == 0) { return -1; } int sz = getSize(buf, off); if (buf.length < off + sz || sz < 1) { return -1; } return (buf[off + sz - 1] & 0xc0) >> 6; } /** * Returns a simple Payload Descriptor, with PartID = 0, the 'start of partition' bit set * according to <tt>startOfPartition</tt>, and all other bits set to 0. * * @param startOfPartition whether to 'start of partition' bit should be set * @return a simple Payload Descriptor, with PartID = 0, the 'start of partition' bit set * according to <tt>startOfPartition</tt>, and all other bits set to 0. */ public static byte[] create(boolean startOfPartition) { byte[] pd = new byte[1]; pd[0] = startOfPartition ? (byte) 0x10 : 0; return pd; } /** * The size in bytes of the Payload Descriptor at offset <tt>offset</tt> in <tt>input</tt>. The * size is between 1 and 6. * * @param input input * @param offset offset * @return The size in bytes of the Payload Descriptor at offset <tt>offset</tt> in * <tt>input</tt>, or -1 if the input is not a valid VP8 Payload Descriptor. The size is * between 1 and 6. */ public static int getSize(byte[] input, int offset) { if (!isValid(input, offset)) return -1; if ((input[offset] & X_BIT) == 0) return 1; int size = 2; if ((input[offset + 1] & I_BIT) != 0) { size++; if ((input[offset + 2] & M_BIT) != 0) size++; } if ((input[offset + 1] & L_BIT) != 0) size++; if ((input[offset + 1] & (T_BIT | K_BIT)) != 0) size++; return size; } /** * Gets the value of the PictureID field of a VP8 Payload Descriptor. * * @param input * @param offset * @return the value of the PictureID field of a VP8 Payload Descriptor, or -1 if the fields is * not present. */ private static int getPictureId(byte[] input, int offset) { if (!isValid(input, offset)) return -1; if ((input[offset] & X_BIT) == 0 || (input[offset + 1] & I_BIT) == 0) return -1; boolean isLong = (input[offset + 2] & M_BIT) != 0; if (isLong) return (input[offset + 2] & 0x7f) << 8 | (input[offset + 3] & 0xff); else return input[offset + 2] & 0x7f; } public static boolean isValid(byte[] input, int offset) { return true; } /** * Checks whether the '<tt>start of partition</tt>' bit is set in the VP8 Payload Descriptor at * offset <tt>offset</tt> in <tt>input</tt>. * * @param input input * @param offset offset * @return <tt>true</tt> if the '<tt>start of partition</tt>' bit is set, <tt>false</tt> * otherwise. */ public static boolean isStartOfPartition(byte[] input, int offset) { return (input[offset] & S_BIT) != 0; } /** * Returns <tt>true</tt> if both the '<tt>start of partition</tt>' bit is set and the * <tt>PID</tt> fields has value 0 in the VP8 Payload Descriptor at offset <tt>offset</tt> in * <tt>input</tt>. * * @param input * @param offset * @return <tt>true</tt> if both the '<tt>start of partition</tt>' bit is set and the * <tt>PID</tt> fields has value 0 in the VP8 Payload Descriptor at offset <tt>offset</tt> * in <tt>input</tt>. */ public static boolean isStartOfFrame(byte[] input, int offset) { return isStartOfPartition(input, offset) && getPartitionId(input, offset) == 0; } /** * Returns the value of the <tt>PID</tt> (partition ID) field of the VP8 Payload Descriptor at * offset <tt>offset</tt> in <tt>input</tt>. * * @param input * @param offset * @return the value of the <tt>PID</tt> (partition ID) field of the VP8 Payload Descriptor at * offset <tt>offset</tt> in <tt>input</tt>. */ public static int getPartitionId(byte[] input, int offset) { return input[offset] & 0x07; } } /** * A class that represents the VP8 Payload Header structure defined in {@link * "http://tools.ietf.org/html/draft-ietf-payload-vp8-10"} */ public static class VP8PayloadHeader { /** S bit of the Payload Descriptor. */ private static final byte S_BIT = (byte) 0x01; /** * Returns true if the <tt>P</tt> (inverse key frame flag) field of the VP8 Payload Header at * offset <tt>offset</tt> in <tt>input</tt> is 0. * * @return true if the <tt>P</tt> (inverse key frame flag) field of the VP8 Payload Header at * offset <tt>offset</tt> in <tt>input</tt> is 0, false otherwise. */ public static boolean isKeyFrame(byte[] input, int offset) { // When set to 0 the current frame is a key frame. When set to 1 // the current frame is an interframe. Defined in [RFC6386] return (input[offset] & S_BIT) == 0; } } /** A simple container for a <tt>byte[]</tt> and an integer. */ private static class Container { /** This <tt>Container</tt>'s data. */ private byte[] buf; /** Length used. */ private int len = 0; } }
/** * The <tt>SimulcastReceiver</tt> of a <tt>SimulcastEngine</tt> receives the simulcast streams from * a simulcast enabled participant and manages 1 or more <tt>SimulcastLayer</tt>s. It fires a * property change event whenever the simulcast layers that it manages change. * * <p>This class is thread safe. * * @author George Politis * @author Lyubomir Marinov */ public class SimulcastReceiver extends PropertyChangeNotifier { /** * The <tt>Logger</tt> used by the <tt>ReceivingLayers</tt> class and its instances to print debug * information. */ private static final Logger logger = Logger.getLogger(SimulcastReceiver.class); /** * The name of the property that gets fired when there's a change in the simulcast layers that * this receiver manages. */ public static final String SIMULCAST_LAYERS_PNAME = SimulcastReceiver.class.getName() + ".simulcastLayers"; /** * The number of (video) frames which defines the interval of time (indirectly) during which a * {@code SimulcastLayer} needs to receive data from its remote peer or it will be declared * paused/stopped/not streaming by its {@code SimulcastReceiver}. */ static final int TIMEOUT_ON_FRAME_COUNT = 5; /** The pool of threads utilized by this class. */ private static final ExecutorService executorService = ExecutorUtils.newCachedThreadPool(true, SimulcastReceiver.class.getName()); /** Helper object that <tt>SwitchingSimulcastSender</tt> instances use to build JSON messages. */ private static final SimulcastMessagesMapper mapper = new SimulcastMessagesMapper(); /** The <tt>SimulcastEngine</tt> that owns this receiver. */ private final SimulcastEngine simulcastEngine; /** The simulcast layers of this <tt>VideoChannel</tt>. */ private SimulcastLayer[] simulcastLayers; /** * Indicates whether we're receiving native or non-native simulcast from the associated endpoint. * It determines whether the bridge should send messages over the data channels to manage the * non-native simulcast. In the case of native simulcast, there's nothing to do for the bridge. * * <p>NOTE that at the time of this writing we only support native simulcast. Last time we tried * non-native simulcast there was no way to limit the bitrate of lower layer streams and thus * there was no point in implementing non-native simulcast. * * <p>NOTE^2 This has changed recently with the webrtc stack automatically limiting the stream * bitrate based on its resolution (see commit 1c7d48d431e098ba42fa6bd9f1cfe69a703edee5 in the * webrtc git repository). So it might be something that we will want to implement in the future * for browsers that don't support native simulcast (Temasys). */ private boolean nativeSimulcast = true; /** * The history of the order/sequence of receipt of (video) frames by {@link #simulcastLayers}. * Used in an attempt to speed up the detection of paused/stopped {@code SimulcastLayer}s by * counting (video) frames. */ private final List<SimulcastLayer> simulcastLayerFrameHistory = new LinkedList<SimulcastLayer>(); /** * Ctor. * * @param simulcastEngine the <tt>SimulcastEngine</tt> that owns this receiver. */ public SimulcastReceiver(SimulcastEngine simulcastEngine) { this.simulcastEngine = simulcastEngine; } /** * Gets the <tt>SimulcastEngine</tt> that owns this receiver. * * @return the <tt>SimulcastEngine</tt> that owns this receiver. */ public SimulcastEngine getSimulcastEngine() { return this.simulcastEngine; } /** * Returns true if the endpoint has signaled two or more simulcast layers. * * @return true if the endpoint has signaled two or more simulcast layers, false otherwise. */ public boolean hasLayers() { SimulcastLayer[] sl = simulcastLayers; return sl != null && sl.length != 0; } /** * Returns a <tt>SimulcastLayer</tt> that is the closest match to the target order, or null if * simulcast hasn't been configured for this receiver. * * @param targetOrder the simulcast layer target order. * @return a <tt>SimulcastLayer</tt> that is the closest match to the target order, or null. */ public SimulcastLayer getSimulcastLayer(int targetOrder) { SimulcastLayer[] layers = getSimulcastLayers(); if (layers == null || layers.length == 0) { return null; } // Iterate through the simulcast layers that we own and return the one // that matches best the targetOrder parameter. SimulcastLayer next = layers[0]; for (int i = 1; i < Math.min(targetOrder + 1, layers.length); i++) { if (!layers[i].isStreaming()) { break; } next = layers[i]; } return next; } /** * Gets the simulcast layers of this simulcast manager in a new <tt>SortedSet</tt> so that the * caller won't have to worry about the structure changing by some other thread. * * @return the simulcast layers of this receiver in a new sorted set if simulcast is signaled, or * null. */ public SimulcastLayer[] getSimulcastLayers() { return simulcastLayers; } /** * Sets the simulcast layers for this receiver and fires an event about it. * * @param simulcastLayers the simulcast layers for this receiver. */ public void setSimulcastLayers(SimulcastLayer[] simulcastLayers) { this.simulcastLayers = simulcastLayers; if (logger.isInfoEnabled()) { if (simulcastLayers == null) { logInfo("Simulcast disabled."); } else { for (SimulcastLayer l : simulcastLayers) { logInfo(l.getOrder() + ": " + l.getPrimarySSRC()); } } } executorService.execute( new Runnable() { public void run() { firePropertyChange(SIMULCAST_LAYERS_PNAME, null, null); } }); // TODO If simulcastLayers has changed, then simulcastLayerFrameHistory // has very likely become irrelevant. In other words, clear // simulcastLayerFrameHistory. } /** * Notifies this instance that a <tt>DatagramPacket</tt> packet received on the data * <tt>DatagramSocket</tt> of this <tt>Channel</tt> has been accepted for further processing * within Jitsi Videobridge. * * @param pkt the accepted <tt>RawPacket</tt>. */ public void accepted(RawPacket pkt) { // With native simulcast we don't have a notification when a stream // has started/stopped. The simulcast manager implements a timeout // for the high quality stream and it needs to be notified when // the channel has accepted a datagram packet for the timeout to // function correctly. if (!hasLayers() || pkt == null) { return; } // Find the layer that corresponds to this packet. int acceptedSSRC = pkt.getSSRC(); SimulcastLayer[] layers = getSimulcastLayers(); SimulcastLayer acceptedLayer = null; for (SimulcastLayer layer : layers) { // We only care about the primary SSRC and not the RTX ssrc (or // future FEC ssrc). if ((int) layer.getPrimarySSRC() == acceptedSSRC) { acceptedLayer = layer; break; } } // If this is not an RTP packet or if we can't find an accepted // layer, log and return as it makes no sense to continue in this // situation. if (acceptedLayer == null) { return; } // There are sequences of packets with increasing timestamps but without // the marker bit set. Supposedly, they are probes to detect whether the // bandwidth may increase. We think that they should cause neither the // start nor the stop of any SimulcastLayer. // XXX There's RawPacket#getPayloadLength() but the implementation // includes pkt.paddingSize at the time of this writing and we do not // know whether that's going to stay that way. int pktPayloadLength = pkt.getLength() - pkt.getHeaderLength(); int pktPaddingSize = pkt.getPaddingSize(); if (pktPayloadLength <= pktPaddingSize) { if (logger.isTraceEnabled()) { logger.trace( "pkt.payloadLength= " + pktPayloadLength + " <= pkt.paddingSize= " + pktPaddingSize + "(" + pkt.getSequenceNumber() + ")"); } return; } // NOTE(gp) we expect the base layer to be always on, so we never touch // it or starve it. // XXX Refer to the implementation of // SimulcastLayer#touch(boolean, RawPacket) for an explanation of why we // chose to use a return value. boolean frameStarted = acceptedLayer.touch(pkt); if (frameStarted) simulcastLayerFrameStarted(acceptedLayer, pkt, layers); } /** * Maybe send a data channel command to the associated <tt>Endpoint</tt> to make it start * streaming its hq stream, if it's being watched by some receiver. */ public void maybeSendStartHighQualityStreamCommand() { if (nativeSimulcast || !hasLayers()) { // In native simulcast the client adjusts its layers autonomously so // we don't need (nor we can) to control it with data channel // messages. return; } Endpoint newEndpoint = getSimulcastEngine().getVideoChannel().getEndpoint(); SimulcastLayer[] newSimulcastLayers = getSimulcastLayers(); SctpConnection sctpConnection; if (newSimulcastLayers == null || newSimulcastLayers.length <= 1 /* newEndpoint != null is implied */ || (sctpConnection = newEndpoint.getSctpConnection()) == null || !sctpConnection.isReady() || sctpConnection.isExpired()) { return; } // we have a new endpoint and it has an SCTP connection that is // ready and not expired. if somebody else is watching the new // endpoint, start its hq stream. boolean startHighQualityStream = false; for (Endpoint e : getSimulcastEngine().getVideoChannel().getContent().getConference().getEndpoints()) { // TODO(gp) need some synchronization here. What if the // selected endpoint changes while we're in the loop? if (e == newEndpoint) continue; Endpoint eSelectedEndpoint = e.getEffectivelySelectedEndpoint(); if (newEndpoint == eSelectedEndpoint) { // somebody is watching the new endpoint or somebody has not // yet signaled its selected endpoint to the bridge, start // the hq stream. if (logger.isDebugEnabled()) { Map<String, Object> map = new HashMap<String, Object>(3); map.put("e", e); map.put("newEndpoint", newEndpoint); map.put("maybe", eSelectedEndpoint == null ? "(maybe) " : ""); StringCompiler sc = new StringCompiler(map).c("{e.id} is {maybe} watching {newEndpoint.id}."); logDebug(sc.toString().replaceAll("\\s+", " ")); } startHighQualityStream = true; break; } } if (startHighQualityStream) { // TODO(gp) this assumes only a single hq stream. logDebug( getSimulcastEngine().getVideoChannel().getEndpoint().getID() + " notifies " + newEndpoint.getID() + " to start its HQ stream."); SimulcastLayer hqLayer = newSimulcastLayers[newSimulcastLayers.length - 1]; ; StartSimulcastLayerCommand command = new StartSimulcastLayerCommand(hqLayer); String json = mapper.toJson(command); try { newEndpoint.sendMessageOnDataChannel(json); } catch (IOException e) { logError(newEndpoint.getID() + " failed to send message on data channel.", e); } } } /** * Maybe send a data channel command to he associated simulcast sender to make it stop streaming * its hq stream, if it's not being watched by any participant. */ public void maybeSendStopHighQualityStreamCommand() { if (nativeSimulcast || !hasLayers()) { // In native simulcast the client adjusts its layers autonomously so // we don't need (nor we can) to control it with data channel // messages. return; } Endpoint oldEndpoint = getSimulcastEngine().getVideoChannel().getEndpoint(); SimulcastLayer[] oldSimulcastLayers = getSimulcastLayers(); SctpConnection sctpConnection; if (oldSimulcastLayers != null && oldSimulcastLayers.length > 1 /* oldEndpoint != null is implied*/ && (sctpConnection = oldEndpoint.getSctpConnection()) != null && sctpConnection.isReady() && !sctpConnection.isExpired()) { // we have an old endpoint and it has an SCTP connection that is // ready and not expired. if nobody else is watching the old // endpoint, stop its hq stream. boolean stopHighQualityStream = true; for (Endpoint e : getSimulcastEngine().getVideoChannel().getContent().getConference().getEndpoints()) { // TODO(gp) need some synchronization here. What if the selected // endpoint changes while we're in the loop? if (oldEndpoint != e && (oldEndpoint == e.getEffectivelySelectedEndpoint()) || e.getEffectivelySelectedEndpoint() == null) { // somebody is watching the old endpoint or somebody has not // yet signaled its selected endpoint to the bridge, don't // stop the hq stream. stopHighQualityStream = false; break; } } if (stopHighQualityStream) { // TODO(gp) this assumes only a single hq stream. logDebug( getSimulcastEngine().getVideoChannel().getEndpoint().getID() + " notifies " + oldEndpoint.getID() + " to stop " + "its HQ stream."); SimulcastLayer hqLayer = oldSimulcastLayers[oldSimulcastLayers.length - 1]; StopSimulcastLayerCommand command = new StopSimulcastLayerCommand(hqLayer); String json = mapper.toJson(command); try { oldEndpoint.sendMessageOnDataChannel(json); } catch (IOException e1) { logError(oldEndpoint.getID() + " failed to send " + "message on data channel.", e1); } } } } private void logDebug(String msg) { if (logger.isDebugEnabled()) { msg = getSimulcastEngine().getVideoChannel().getEndpoint().getID() + ": " + msg; logger.debug(msg); } } private void logWarn(String msg) { if (logger.isWarnEnabled()) { msg = getSimulcastEngine().getVideoChannel().getEndpoint().getID() + ": " + msg; logger.warn(msg); } } private void logError(String msg, Throwable e) { msg = getSimulcastEngine().getVideoChannel().getEndpoint().getID() + ": " + msg; logger.error(msg, e); } private void logInfo(String msg) { if (logger.isInfoEnabled()) { msg = getSimulcastEngine().getVideoChannel().getEndpoint().getID() + ": " + msg; logger.info(msg); } } /** * Notifies this {@code SimulcastReceiver} that a specific {@code SimulcastReceiver} has detected * the start of a new video frame in the RTP stream that it represents. Determines whether any of * {@link #simulcastLayers} other than {@code source} have been paused/stopped by the remote peer. * The determination is based on counting (video) frames. * * @param source the {@code SimulcastLayer} which is the source of the event i.e. which has * detected the start of a new video frame in the RTP stream that it represents * @param pkt the {@code RawPacket} which was received by {@code source} and possibly influenced * the decision that a new view frame was started in the RTP stream represented by {@code * source} * @param layers the set of {@code SimulcastLayer}s managed by this {@code SimulcastReceiver}. * Explicitly provided to the method in order to avoid invocations of {@link * #getSimulcastLayers()} because the latter makes a copy at the time of this writing. */ private void simulcastLayerFrameStarted( SimulcastLayer source, RawPacket pkt, SimulcastLayer[] layers) { // Allow the value of the constant TIMEOUT_ON_FRAME_COUNT to disable (at // compile time) the frame-based approach to the detection of layer // drops. if (TIMEOUT_ON_FRAME_COUNT <= 1) return; // Timeouts in layers caused by source may occur only based on the span // (of time or received frames) during which source has received // TIMEOUT_ON_FRAME_COUNT number of frames. The current method // invocation signals the receipt of 1 frame by source. int indexOfLastSourceOccurrenceInHistory = -1; int sourceFrameCount = 0; int ix = 0; for (Iterator<SimulcastLayer> it = simulcastLayerFrameHistory.iterator(); it.hasNext(); ++ix) { if (it.next() == source) { if (indexOfLastSourceOccurrenceInHistory != -1) { // Prune simulcastLayerFrameHistory so that it does not // become unnecessarily long. it.remove(); } else if (++sourceFrameCount >= TIMEOUT_ON_FRAME_COUNT - 1) { // The span of TIMEOUT_ON_FRAME_COUNT number of frames // received by source only is to be examined for the // purposes of timeouts. The current method invocations // signals the receipt of 1 frame by source so // TIMEOUT_ON_FRAME_COUNT - 1 occurrences of source in // simulcastLayerFrameHistory is enough. indexOfLastSourceOccurrenceInHistory = ix; } } } if (indexOfLastSourceOccurrenceInHistory != -1) { // Presumably, if a SimulcastLayer is active, all SimulcastLayers // before it (according to SimulcastLayer's order) are active as // well. Consequently, timeouts may occur in SimulcastLayers which // are after source. boolean maybeTimeout = false; for (SimulcastLayer layer : layers) { if (maybeTimeout) { // There's no point in timing layer out if it's timed out // already. if (layer.isStreaming()) { maybeTimeout(source, pkt, layer, indexOfLastSourceOccurrenceInHistory); } } else if (layer == source) { maybeTimeout = true; } } } // As previously stated, the current method invocation signals the // receipt of 1 frame by source. simulcastLayerFrameHistory.add(0, source); // TODO Prune simulcastLayerFrameHistory by forgetting so that it does // not become too long. } /** * Determines whether {@code effect} has been paused/stopped by the remote peer. The determination * is based on counting frames and is triggered by the receipt of (a piece of) a new (video) frame * by {@code cause}. * * @param cause the {@code SimulcastLayer} which has received (a piece of) a new (video) frame and * has thus triggered a check on {@code effect} * @param pkt the {@code RawPacket} which was received by {@code cause} and possibly influenced * the decision to trigger a check on {@code effect} * @param effect the {@code SimulcastLayer} which is to be checked whether it looks like it has * been paused/stopped by the remote peer * @param endIndexInSimulcastLayerFrameHistory */ private void maybeTimeout( SimulcastLayer cause, RawPacket pkt, SimulcastLayer effect, int endIndexInSimulcastLayerFrameHistory) { Iterator<SimulcastLayer> it = simulcastLayerFrameHistory.iterator(); boolean timeout = true; for (int ix = 0; it.hasNext() && ix < endIndexInSimulcastLayerFrameHistory; ++ix) { if (it.next() == effect) { timeout = false; break; } } if (timeout) { effect.maybeTimeout(pkt); if (!effect.isStreaming()) { // Since effect has been determined to have been paused/stopped // by the remote peer, its possible presence in // simulcastLayerFrameHistory is irrelevant now. In other words, // remove effect from simulcastLayerFrameHistory. while (it.hasNext()) { if (it.next() == effect) it.remove(); } } } } }
class DBPortPool extends SimplePool<DBPort> { static class Holder { Holder(MongoOptions options) { _options = options; } DBPortPool get(InetSocketAddress addr) { DBPortPool p = _pools.get(addr); if (p != null) return p; synchronized (_pools) { p = _pools.get(addr); if (p != null) { return p; } p = new DBPortPool(addr, _options); _pools.put(addr, p); String name = "com.mongodb:type=ConnectionPool,host=" + addr.toString().replace(':', '_'); try { ObjectName on = new ObjectName(name); if (_server.isRegistered(on)) { _server.unregisterMBean(on); Bytes.LOGGER.log( Level.INFO, "multiple Mongo instances for same host, jmx numbers might be off"); } _server.registerMBean(p, on); } catch (JMException e) { Bytes.LOGGER.log(Level.WARNING, "jmx registration error, continuing", e); } catch (java.security.AccessControlException e) { Bytes.LOGGER.log(Level.WARNING, "jmx registration error, continuing", e); } } return p; } void close() { synchronized (_pools) { for (DBPortPool p : _pools.values()) { p.close(); } } } final MongoOptions _options; final Map<InetSocketAddress, DBPortPool> _pools = Collections.synchronizedMap(new HashMap<InetSocketAddress, DBPortPool>()); final MBeanServer _server = ManagementFactory.getPlatformMBeanServer(); } // ---- public static class NoMoreConnection extends MongoInternalException { NoMoreConnection(String msg) { super(msg); } } public static class SemaphoresOut extends NoMoreConnection { SemaphoresOut() { super("Out of semaphores to get db connection"); } } public static class ConnectionWaitTimeOut extends NoMoreConnection { ConnectionWaitTimeOut(int timeout) { super("Connection wait timeout after " + timeout + " ms"); } } // ---- DBPortPool(InetSocketAddress addr, MongoOptions options) { super("DBPortPool-" + addr.toString(), options.connectionsPerHost, options.connectionsPerHost); _options = options; _addr = addr; _waitingSem = new Semaphore( _options.connectionsPerHost * _options.threadsAllowedToBlockForConnectionMultiplier); } protected long memSize(DBPort p) { return 0; } protected int pick(int iThink, boolean couldCreate) { final int id = Thread.currentThread().hashCode(); final int s = _availSafe.size(); for (int i = 0; i < s; i++) { DBPort p = _availSafe.get(i); if (p._lastThread == id) return i; } if (couldCreate) return -1; return iThink; } public DBPort get() { DBPort port = null; if (!_waitingSem.tryAcquire()) throw new SemaphoresOut(); try { port = get(_options.maxWaitTime); } finally { _waitingSem.release(); } if (port == null) throw new ConnectionWaitTimeOut(_options.maxWaitTime); port._lastThread = Thread.currentThread().hashCode(); return port; } void gotError(Exception e) { if (e instanceof java.nio.channels.ClosedByInterruptException || e instanceof InterruptedException) { // this is probably a request that is taking too long // so usually doesn't mean there is a real db problem return; } if (e instanceof java.net.SocketTimeoutException && _options.socketTimeout > 0) { // we don't want to clear the port pool for 1 connection timing out return; } // We don't want to clear the entire pool for the occasional error. if (e instanceof SocketException) { if (recentFailures < ALLOWED_ERRORS_BEFORE_CLEAR) { return; } } Bytes.LOGGER.log(Level.INFO, "emptying DBPortPool b/c of error", e); clear(); } void close() { clear(); } public void cleanup(DBPort p) { p.close(); } public boolean ok(DBPort t) { return _addr.equals(t._addr); } protected DBPort createNew() throws MongoInternalException { try { return new DBPort(_addr, this, _options); } catch (IOException ioe) { throw new MongoInternalException("can't create port to:" + _addr, ioe); } } public int getRecentFailures() { return recentFailures; } public void incrementRecentFailures() { _logger.warning("Failure recorded:" + _addr.toString()); this.recentFailures++; } public void resetRecentFailures() { if (this.recentFailures > 0) { _logger.warning("Successful Request. Reseting recent failures:" + _addr.toString()); } this.recentFailures = 0; } final MongoOptions _options; private final Semaphore _waitingSem; final InetSocketAddress _addr; boolean _everWorked = false; public static final Integer ALLOWED_ERRORS_BEFORE_CLEAR = Integer.valueOf(System.getProperty("MONGO.ERRORS_BEFORE_CLEAR", "5")); private Logger _logger = Logger.getLogger(DBPortPool.class.toString()); /** The number of failures that this port pool has recently experienced. */ private int recentFailures = 0; }
/** * Mock {@link ChatRoom} implementation. * * @author Pawel Domas */ public class MockMultiUserChat extends AbstractChatRoom implements ChatRoom2 { /** The logger */ private static final Logger logger = Logger.getLogger(MockMultiUserChat.class); private final String roomName; private final ProtocolProviderService protocolProvider; private volatile boolean isJoined; private final List<ChatRoomMember> members = new CopyOnWriteArrayList<ChatRoomMember>(); private ChatRoomMember me; /** * Listeners that will be notified of changes in member status in the room such as member joined, * left or being kicked or dropped. */ private final Vector<ChatRoomMemberPresenceListener> memberListeners = new Vector<ChatRoomMemberPresenceListener>(); private final Vector<ChatRoomLocalUserRoleListener> localUserRoleListeners = new Vector<ChatRoomLocalUserRoleListener>(); private final Vector<ChatRoomMemberRoleListener> memberRoleListeners = new Vector<ChatRoomMemberRoleListener>(); public MockMultiUserChat(String roomName, ProtocolProviderService protocolProviderService) { this.roomName = roomName; this.protocolProvider = protocolProviderService; } @Override public String getName() { return roomName; } @Override public String getIdentifier() { return null; } @Override public void join() throws OperationFailedException { joinAs( getParentProvider() .getAccountID() .getAccountPropertyString(ProtocolProviderFactory.DISPLAY_NAME)); } @Override public void join(byte[] password) throws OperationFailedException { join(); } @Override public void joinAs(String nickname) throws OperationFailedException { joinAs(nickname, null); } private String createAddressForName(String nickname) { return roomName + "/" + nickname; } @Override public void joinAs(String nickname, byte[] password) throws OperationFailedException { if (isJoined) throw new OperationFailedException("Alread joined the room", 0); isJoined = true; MockRoomMember member = new MockRoomMember(createAddressForName(nickname), this); // FIXME: for mock purposes we are always the owner on join() boolean isOwner = true; // = members.size() == 0; synchronized (members) { members.add(member); me = member; fireMemberPresenceEvent(me, me, ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED, null); } ChatRoomMemberRole oldRole = me.getRole(); if (isOwner) { me.setRole(ChatRoomMemberRole.OWNER); } fireLocalUserRoleEvent(me, oldRole, true); } public MockRoomMember mockOwnerJoin(String name) { MockRoomMember member = new MockRoomMember(name, this); member.setRole(ChatRoomMemberRole.OWNER); mockJoin(member); return member; } public MockRoomMember mockJoin(String nickname) { return mockJoin(createMockRoomMember(nickname)); } public MockRoomMember createMockRoomMember(String nickname) { return new MockRoomMember(createAddressForName(nickname), this); } public MockRoomMember mockJoin(MockRoomMember member) { synchronized (members) { members.add(member); fireMemberPresenceEvent( member, member, ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED, null); return member; } } public void mockLeave(String memberName) { for (ChatRoomMember member : members) { if (member.getName().equals(memberName)) { mockLeave((MockRoomMember) member); } } } private void mockLeave(MockRoomMember member) { synchronized (members) { if (!members.remove(member)) { throw new RuntimeException("Member is not in the room " + member); } fireMemberPresenceEvent(member, member, ChatRoomMemberPresenceChangeEvent.MEMBER_LEFT, null); } } @Override public boolean isJoined() { return isJoined; } @Override public void leave() { if (!isJoined) return; isJoined = false; synchronized (members) { members.remove(me); fireMemberPresenceEvent(me, me, ChatRoomMemberPresenceChangeEvent.MEMBER_LEFT, null); } me = null; } @Override public String getSubject() { return null; } @Override public void setSubject(String subject) throws OperationFailedException {} @Override public String getUserNickname() { return null; } @Override public ChatRoomMemberRole getUserRole() { return null; } @Override public void setLocalUserRole(ChatRoomMemberRole role) throws OperationFailedException {} @Override public void setUserNickname(String nickname) throws OperationFailedException {} @Override public void addMemberPresenceListener(ChatRoomMemberPresenceListener listener) { synchronized (memberListeners) { memberListeners.add(listener); } } @Override public void removeMemberPresenceListener(ChatRoomMemberPresenceListener listener) { synchronized (memberListeners) { memberListeners.remove(listener); } } @Override public void addLocalUserRoleListener(ChatRoomLocalUserRoleListener listener) { localUserRoleListeners.add(listener); } @Override public void removelocalUserRoleListener(ChatRoomLocalUserRoleListener listener) { localUserRoleListeners.remove(listener); } @Override public void addMemberRoleListener(ChatRoomMemberRoleListener listener) { memberRoleListeners.add(listener); } @Override public void removeMemberRoleListener(ChatRoomMemberRoleListener listener) { memberRoleListeners.remove(listener); } @Override public void addPropertyChangeListener(ChatRoomPropertyChangeListener listener) {} @Override public void removePropertyChangeListener(ChatRoomPropertyChangeListener listener) {} @Override public void addMemberPropertyChangeListener(ChatRoomMemberPropertyChangeListener listener) {} @Override public void removeMemberPropertyChangeListener(ChatRoomMemberPropertyChangeListener listener) {} @Override public void invite(String userAddress, String reason) {} @Override public List<ChatRoomMember> getMembers() { return members; } @Override public int getMembersCount() { return members.size(); } @Override public void addMessageListener(ChatRoomMessageListener listener) {} @Override public void removeMessageListener(ChatRoomMessageListener listener) {} @Override public Message createMessage( byte[] content, String contentType, String contentEncoding, String subject) { return null; } @Override public Message createMessage(String messageText) { return null; } @Override public void sendMessage(Message message) throws OperationFailedException {} @Override public ProtocolProviderService getParentProvider() { return protocolProvider; } @Override public Iterator<ChatRoomMember> getBanList() throws OperationFailedException { return null; } @Override public void banParticipant(ChatRoomMember chatRoomMember, String reason) throws OperationFailedException {} @Override public void kickParticipant(ChatRoomMember chatRoomMember, String reason) throws OperationFailedException {} @Override public ChatRoomConfigurationForm getConfigurationForm() throws OperationFailedException { return null; } @Override public boolean isSystem() { return false; } @Override public boolean isPersistent() { return false; } @Override public Contact getPrivateContactByNickname(String name) { return null; } @Override public void grantAdmin(String address) {} @Override public void grantMembership(String address) {} @Override public void grantModerator(String nickname) { MockRoomMember member = findMember(nickname); if (member == null) { logger.error("Member not found for nickname: " + nickname); return; } if (ChatRoomMemberRole.MODERATOR.compareTo(member.getRole()) >= 0) { // No action required return; } ChatRoomMemberRole oldRole = member.getRole(); member.setRole(ChatRoomMemberRole.MODERATOR); fireMemberRoleEvent(member, oldRole); } private MockRoomMember findMember(String nickname) { for (ChatRoomMember member : members) { if (nickname.equals(member.getName())) return (MockRoomMember) member; } return null; } @Override public void grantOwnership(String address) {} @Override public void grantVoice(String nickname) {} @Override public void revokeAdmin(String address) {} @Override public void revokeMembership(String address) {} @Override public void revokeModerator(String nickname) {} @Override public void revokeOwnership(String address) {} @Override public void revokeVoice(String nickname) {} @Override public ConferenceDescription publishConference(ConferenceDescription cd, String name) { return null; } @Override public void updatePrivateContactPresenceStatus(String nickname) {} @Override public void updatePrivateContactPresenceStatus(Contact contact) {} @Override public boolean destroy(String reason, String alternateAddress) { return false; } @Override public List<String> getMembersWhiteList() { return null; } @Override public void setMembersWhiteList(List<String> members) {} /** * Creates the corresponding ChatRoomMemberPresenceChangeEvent and notifies all * <tt>ChatRoomMemberPresenceListener</tt>s that a ChatRoomMember has joined or left this * <tt>ChatRoom</tt>. * * @param member the <tt>ChatRoomMember</tt> that changed its presence status * @param actor the <tt>ChatRoomMember</tt> that participated as an actor in this event * @param eventID the identifier of the event * @param eventReason the reason of this event */ private void fireMemberPresenceEvent( ChatRoomMember member, ChatRoomMember actor, String eventID, String eventReason) { ChatRoomMemberPresenceChangeEvent evt = new ChatRoomMemberPresenceChangeEvent(this, member, actor, eventID, eventReason); Iterable<ChatRoomMemberPresenceListener> listeners; synchronized (memberListeners) { listeners = new ArrayList<ChatRoomMemberPresenceListener>(memberListeners); } for (ChatRoomMemberPresenceListener listener : listeners) listener.memberPresenceChanged(evt); } private void fireLocalUserRoleEvent( ChatRoomMember member, ChatRoomMemberRole oldRole, boolean isInitial) { ChatRoomLocalUserRoleChangeEvent evt = new ChatRoomLocalUserRoleChangeEvent(this, oldRole, member.getRole(), isInitial); Iterable<ChatRoomLocalUserRoleListener> listeners; synchronized (localUserRoleListeners) { listeners = new ArrayList<ChatRoomLocalUserRoleListener>(localUserRoleListeners); } for (ChatRoomLocalUserRoleListener listener : listeners) listener.localUserRoleChanged(evt); } private void fireMemberRoleEvent(ChatRoomMember member, ChatRoomMemberRole oldRole) { ChatRoomMemberRoleChangeEvent evt = new ChatRoomMemberRoleChangeEvent(this, member, oldRole, member.getRole()); Iterable<ChatRoomMemberRoleListener> listeners; synchronized (memberRoleListeners) { listeners = new ArrayList<ChatRoomMemberRoleListener>(memberRoleListeners); } for (ChatRoomMemberRoleListener listener : listeners) listener.memberRoleChanged(evt); } @Override public String toString() { return "MockMUC@" + hashCode() + "[" + this.roomName + ", " + protocolProvider + "]"; } @Override public XmppChatMember findChatMember(String mucJid) { String nick = MucUtil.extractNickname(mucJid); return findMember(nick); } }
/** Provides a device profile. */ public final class DeviceProfile implements Cloneable, Comparable<DeviceProfile> { // INNER TYPES /** The various capture clock sources. */ public static enum CaptureClockSource { INTERNAL, EXTERNAL_FALLING, EXTERNAL_RISING; } /** The various interfaces of the device. */ public static enum DeviceInterface { SERIAL, NETWORK, USB; } /** The various numbering schemes. */ public static enum NumberingScheme { DEFAULT, INSIDE, OUTSIDE; } /** The various types of triggers. */ public static enum TriggerType { SIMPLE, COMPLEX; } // CONSTANTS /** The short (single word) type of the device described in this profile */ public static final String DEVICE_TYPE = "device.type"; /** A longer description of the device */ public static final String DEVICE_DESCRIPTION = "device.description"; /** The device interface, currently SERIAL only */ public static final String DEVICE_INTERFACE = "device.interface"; /** The device's native clockspeed, in Hertz. */ public static final String DEVICE_CLOCKSPEED = "device.clockspeed"; /** * Whether or not double-data-rate is supported by the device (also known as the "demux"-mode). */ public static final String DEVICE_SUPPORTS_DDR = "device.supports_ddr"; /** Supported sample rates in Hertz, separated by comma's */ public static final String DEVICE_SAMPLERATES = "device.samplerates"; /** What capture clocks are supported */ public static final String DEVICE_CAPTURECLOCK = "device.captureclock"; /** The supported capture sizes, in bytes */ public static final String DEVICE_CAPTURESIZES = "device.capturesizes"; /** Whether or not the noise filter is supported */ public static final String DEVICE_FEATURE_NOISEFILTER = "device.feature.noisefilter"; /** Whether or not Run-Length encoding is supported */ public static final String DEVICE_FEATURE_RLE = "device.feature.rle"; /** Whether or not a testing mode is supported. */ public static final String DEVICE_FEATURE_TEST_MODE = "device.feature.testmode"; /** Whether or not triggers are supported */ public static final String DEVICE_FEATURE_TRIGGERS = "device.feature.triggers"; /** The number of trigger stages */ public static final String DEVICE_TRIGGER_STAGES = "device.trigger.stages"; /** Whether or not "complex" triggers are supported */ public static final String DEVICE_TRIGGER_COMPLEX = "device.trigger.complex"; /** The total number of channels usable for capturing */ public static final String DEVICE_CHANNEL_COUNT = "device.channel.count"; /** * The number of channels groups, together with the channel count determines the channels per * group */ public static final String DEVICE_CHANNEL_GROUPS = "device.channel.groups"; /** Whether the capture size is limited by the enabled channel groups */ public static final String DEVICE_CAPTURESIZE_BOUND = "device.capturesize.bound"; /** What channel numbering schemes are supported by the device. */ public static final String DEVICE_CHANNEL_NUMBERING_SCHEMES = "device.channel.numberingschemes"; /** * Is a delay after opening the port and device detection needed? (0 = no delay, >0 = delay in * milliseconds) */ public static final String DEVICE_OPEN_PORT_DELAY = "device.open.portdelay"; /** The receive timeout (100 = default, in milliseconds) */ public static final String DEVICE_RECEIVE_TIMEOUT = "device.receive.timeout"; /** * Which metadata keys correspond to this device profile? Value is a comma-separated list of * (double quoted) names. */ public static final String DEVICE_METADATA_KEYS = "device.metadata.keys"; /** * In which order are samples sent back from the device? If <code>true</code> then last sample * first, if <code>false</code> then first sample first. */ public static final String DEVICE_SAMPLE_REVERSE_ORDER = "device.samples.reverseOrder"; /** In case of a serial port, does the DTR-line need to be high (= true) or low (= false)? */ public static final String DEVICE_OPEN_PORT_DTR = "device.open.portdtr"; /** Filename of the actual file picked up by Felix's FileInstall. */ public static final String FELIX_FILEINSTALL_FILENAME = "felix.fileinstall.filename"; /** Service PID of this device profile. */ private static final String FELIX_SERVICE_PID = "service.pid"; /** Factory Service PID of this device profile. */ private static final String FELIX_SERVICE_FACTORY_PID = "service.factoryPid"; /** All the profile keys that are supported. */ private static final List<String> KNOWN_KEYS = Arrays.asList( new String[] { DEVICE_TYPE, DEVICE_DESCRIPTION, DEVICE_INTERFACE, DEVICE_CLOCKSPEED, DEVICE_SUPPORTS_DDR, DEVICE_SAMPLERATES, DEVICE_CAPTURECLOCK, DEVICE_CAPTURESIZES, DEVICE_FEATURE_NOISEFILTER, DEVICE_FEATURE_RLE, DEVICE_FEATURE_TEST_MODE, DEVICE_FEATURE_TRIGGERS, DEVICE_TRIGGER_STAGES, DEVICE_TRIGGER_COMPLEX, DEVICE_CHANNEL_COUNT, DEVICE_CHANNEL_GROUPS, DEVICE_CAPTURESIZE_BOUND, DEVICE_CHANNEL_NUMBERING_SCHEMES, DEVICE_OPEN_PORT_DELAY, DEVICE_METADATA_KEYS, DEVICE_SAMPLE_REVERSE_ORDER, DEVICE_OPEN_PORT_DTR, DEVICE_RECEIVE_TIMEOUT, FELIX_FILEINSTALL_FILENAME }); private static final List<String> IGNORED_KEYS = Arrays.asList(new String[] {FELIX_SERVICE_PID, FELIX_SERVICE_FACTORY_PID}); private static final Logger LOG = Logger.getLogger(DeviceProfile.class.getName()); // VARIABLES private final ConcurrentMap<String, String> properties; // CONSTRUCTORS /** Creates a new DeviceProfile. */ public DeviceProfile() { this.properties = new ConcurrentHashMap<String, String>(); } // METHODS /** * @param aFilename * @return */ static final File createFile(final String aFilename) { if (aFilename == null) { throw new IllegalArgumentException("Filename cannot be null!"); } return new File(aFilename.replaceAll("^file:", "")); } /** * Returns a deep copy of this device profile, including all properties. * * @return a deep copy of this device profile, never <code>null</code>. * @see java.lang.Object#clone() */ @Override public DeviceProfile clone() { try { DeviceProfile clone = (DeviceProfile) super.clone(); clone.properties.putAll(this.properties); return clone; } catch (CloneNotSupportedException exception) { throw new IllegalStateException(exception); } } /** {@inheritDoc} */ @Override public int compareTo(DeviceProfile aProfile) { // Issue #123: allow device profiles to be sorted alphabetically... int result = getDescription().compareTo(aProfile.getDescription()); if (result == 0) { result = getType().compareTo(aProfile.getType()); } return result; } /** {@inheritDoc} */ @Override public boolean equals(final Object aObject) { if (this == aObject) { return true; } if ((aObject == null) || !(aObject instanceof DeviceProfile)) { return false; } final DeviceProfile other = (DeviceProfile) aObject; return this.properties.equals(other.properties); } /** * Returns the capture clock sources supported by the device. * * @return an array of capture clock sources, never <code>null</code>. */ public CaptureClockSource[] getCaptureClock() { final String rawValue = this.properties.get(DEVICE_CAPTURECLOCK); final String[] values = rawValue.split(",\\s*"); final CaptureClockSource[] result = new CaptureClockSource[values.length]; for (int i = 0; i < values.length; i++) { result[i] = CaptureClockSource.valueOf(values[i].trim()); } return result; } /** * Returns all supported capture sizes. * * @return an array of capture sizes, in bytes, never <code>null</code>. */ public Integer[] getCaptureSizes() { final String rawValue = this.properties.get(DEVICE_CAPTURESIZES); final String[] values = rawValue.split(",\\s*"); final List<Integer> result = new ArrayList<Integer>(); for (String value : values) { result.add(Integer.valueOf(value.trim())); } Collections.sort( result, NumberUtils.<Integer>createNumberComparator(false /* aSortAscending */)); return result.toArray(new Integer[result.size()]); } /** * Returns the total number of capture channels. * * @return a capture channel count, greater than 0. */ public int getChannelCount() { final String value = this.properties.get(DEVICE_CHANNEL_COUNT); return Integer.parseInt(value); } /** * Returns the total number of channel groups. * * @return a channel group count, greater than 0. */ public int getChannelGroupCount() { final String value = this.properties.get(DEVICE_CHANNEL_GROUPS); return Integer.parseInt(value); } /** * Returns all supported channel numbering schemes. * * @return an array of numbering schemes, never <code>null</code>. */ public NumberingScheme[] getChannelNumberingSchemes() { final String rawValue = this.properties.get(DEVICE_CHANNEL_NUMBERING_SCHEMES); final String[] values = rawValue.split(",\\s*"); final NumberingScheme[] result = new NumberingScheme[values.length]; for (int i = 0; i < result.length; i++) { result[i] = NumberingScheme.valueOf(values[i].trim()); } return result; } /** * Returns the (maximum) capture clock of the device. * * @return a capture clock, in Hertz. */ public int getClockspeed() { final String value = this.properties.get(DEVICE_CLOCKSPEED); return Integer.parseInt(value); } /** * Returns the description of the device this profile denotes. * * @return a device description, never <code>null</code>. */ public String getDescription() { final String result = this.properties.get(DEVICE_DESCRIPTION); return result == null ? "" : (String) result; } /** * Returns the metadata keys that allow identification of this device profile. * * <p>Note: if the returned array contains an empty string value (not <code>null</code>, but * <code>""</code>!), it means that this profile can be used for <em>all</em> devices. * * @return an array of metadata keys this profile supports, never <code>null</code>. */ public String[] getDeviceMetadataKeys() { final String rawValue = this.properties.get(DEVICE_METADATA_KEYS); return StringUtils.tokenizeQuotedStrings(rawValue, ", "); } /** * Returns the interface over which the device communicates. * * @return the device interface, never <code>null</code>. */ public DeviceInterface getInterface() { final String value = this.properties.get(DEVICE_INTERFACE); return DeviceInterface.valueOf(value); } /** * Returns the maximum capture size for the given number of <em>enabled</em> channel groups. * * <p>If the maximum capture size is bound to the number of enabled channel(group)s, this method * will divide the maximum possible capture size by the given group count, otherwise the maximum * capture size will be returned. * * @param aChannelGroups the number of channel groups that should be enabled, > 0 && < channel * group count. * @return a maximum capture size, in bytes, or -1 if no maximum could be determined, or the given * parameter was <tt>0</tt>. * @see #isCaptureSizeBoundToEnabledChannels() * @see #getChannelGroupCount() */ public int getMaximumCaptureSizeFor(final int aChannelGroups) { final Integer[] sizes = getCaptureSizes(); if ((sizes == null) || (sizes.length == 0) || (aChannelGroups == 0)) { return -1; } final int maxSize = sizes[0].intValue(); if (isCaptureSizeBoundToEnabledChannels()) { int indication = maxSize / aChannelGroups; // Issue #58: Search the best matching value... Integer result = null; for (int i = sizes.length - 1; i >= 0; i--) { if (sizes[i].compareTo(Integer.valueOf(indication)) <= 0) { result = sizes[i]; } } return (result == null) ? indication : result.intValue(); } return maxSize; } /** * Returns the delay between opening the port to the device and starting the device detection * cycle. * * @return a delay, in milliseconds, >= 0. */ public int getOpenPortDelay() { final String value = this.properties.get(DEVICE_OPEN_PORT_DELAY); return Integer.parseInt(value); } /** * Returns the (optional) receive timeout. * * <p>WARNING: if no receive timeout is used, the communication essentially results in a * non-blocking I/O operation which can not be cancelled! * * @return the receive timeout, in ms, or <code>null</code> when no receive timeout should be * used. */ public Integer getReceiveTimeout() { final String value = this.properties.get(DEVICE_RECEIVE_TIMEOUT); if (value == null) { return null; } int timeout = Integer.parseInt(value); return (timeout <= 0) ? null : Integer.valueOf(timeout); } /** * Returns all supported sample rates. * * @return an array of sample rates, in Hertz, never <code>null</code>. */ public Integer[] getSampleRates() { final String rawValue = this.properties.get(DEVICE_SAMPLERATES); final String[] values = rawValue.split(",\\s*"); final SortedSet<Integer> result = new TreeSet<Integer>( NumberUtils.<Integer>createNumberComparator(false /* aSortAscending */)); for (String value : values) { result.add(Integer.valueOf(value.trim())); } return result.toArray(new Integer[result.size()]); } /** * Returns the total number of trigger stages (in the complex trigger mode). * * @return a trigger stage count, greater than 0. */ public int getTriggerStages() { final String value = this.properties.get(DEVICE_TRIGGER_STAGES); return Integer.parseInt(value); } /** * Returns the device type this profile denotes. * * @return a device type name, never <code>null</code>. */ public String getType() { final String result = this.properties.get(DEVICE_TYPE); return result == null ? "<unknown>" : result; } /** {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; result = (prime * result) + ((this.properties == null) ? 0 : this.properties.hashCode()); return result; } /** * Returns whether or not the capture size is bound to the number of channels. * * @return <code>true</code> if the capture size is bound to the number of channels, <code>false * </code> otherwise. */ public boolean isCaptureSizeBoundToEnabledChannels() { final String value = this.properties.get(DEVICE_CAPTURESIZE_BOUND); return Boolean.parseBoolean(value); } /** * Returns whether or not the device supports "complex" triggers. * * @return <code>true</code> if complex triggers are supported by the device, <code>false</code> * otherwise. */ public boolean isComplexTriggersSupported() { final String value = this.properties.get(DEVICE_TRIGGER_COMPLEX); return Boolean.parseBoolean(value); } /** * Returns whether or not the device supports "double-data rate" sampling, also known as * "demux"-sampling. * * @return <code>true</code> if DDR is supported by the device, <code>false</code> otherwise. */ public boolean isDoubleDataRateSupported() { final String value = this.properties.get(DEVICE_SUPPORTS_DDR); return Boolean.parseBoolean(value); } /** * Returns whether or not the device supports a noise filter. * * @return <code>true</code> if a noise filter is present in the device, <code>false</code> * otherwise. */ public boolean isNoiseFilterSupported() { final String value = this.properties.get(DEVICE_FEATURE_NOISEFILTER); return Boolean.parseBoolean(value); } /** * Returns whether upon opening the DTR line needs to be high (= <code>true</code>) or low (= * <code>false</code>). * * <p>This method has no meaning if the used interface is <em>not</em> {@link * DeviceInterface#SERIAL}. * * @return <code>true</code> if the DTR line needs to be set upon opening the serial port, <code> * false</code> if the DTR line needs to be reset upon opening the serial port. */ public boolean isOpenPortDtr() { final String value = this.properties.get(DEVICE_OPEN_PORT_DTR); return Boolean.parseBoolean(value); } /** * Returns whether or not the device supports RLE (Run-Length Encoding). * * @return <code>true</code> if a RLE encoder is present in the device, <code>false</code> * otherwise. */ public boolean isRleSupported() { final String value = this.properties.get(DEVICE_FEATURE_RLE); return Boolean.parseBoolean(value); } /** * Returns whether the device send its samples in "reverse" order. * * @return <code>true</code> if samples are send in reverse order (= last sample first), <code> * false</code> otherwise. */ public boolean isSamplesInReverseOrder() { final String rawValue = this.properties.get(DEVICE_SAMPLE_REVERSE_ORDER); return Boolean.parseBoolean(rawValue); } /** * Returns whether or not the device supports a testing mode. * * @return <code>true</code> if testing mode is supported by the device, <code>false</code> * otherwise. */ public boolean isTestModeSupported() { final String value = this.properties.get(DEVICE_FEATURE_TEST_MODE); return Boolean.parseBoolean(value); } /** * Returns whether or not the device supports triggers. * * @return <code>true</code> if the device supports triggers, <code>false</code> otherwise. */ public boolean isTriggerSupported() { final String value = this.properties.get(DEVICE_FEATURE_TRIGGERS); return Boolean.parseBoolean(value); } /** {@inheritDoc} */ @Override public String toString() { return getType(); } /** * Returns the configuration file picked up by Felix's FileInstall bundle. * * @return a configuration file, never <code>null</code>. */ final File getConfigurationFile() { final String value = this.properties.get(FELIX_FILEINSTALL_FILENAME); assert value != null : "Internal error: no fileinstall filename?!"; return createFile(value); } /** @return the properties of this device profile, never <code>null</code>. */ final Dictionary<String, String> getProperties() { return new Hashtable<String, String>(this.properties); } /** @param aProperties the updated properties. */ @SuppressWarnings("rawtypes") final void setProperties(final Dictionary aProperties) { final Map<String, String> newProps = new HashMap<String, String>(); Enumeration keys = aProperties.keys(); while (keys.hasMoreElements()) { final String key = (String) keys.nextElement(); if (!KNOWN_KEYS.contains(key) && !IGNORED_KEYS.contains(key)) { LOG.log(Level.WARNING, "Unknown/unsupported profile key: " + key); continue; } final String value = aProperties.get(key).toString(); newProps.put(key, value.trim()); } // Verify whether all known keys are defined... final List<String> checkedKeys = new ArrayList<String>(KNOWN_KEYS); checkedKeys.removeAll(newProps.keySet()); if (!checkedKeys.isEmpty()) { throw new IllegalArgumentException( "Profile settings not complete! Missing keys are: " + checkedKeys.toString()); } this.properties.putAll(newProps); LOG.log( Level.INFO, "New device profile settings applied for {1} ({0}) ...", // new Object[] {getType(), getDescription()}); } }
/** * This is the container for an instance of a site on a single server. This can be access via * __instance__ * * @anonymous name : {local}, isField : {true}, desc : {Refers to the site being run.}, type: * {library} * @anonymous name : {core}, isField : {true}, desc : {Refers to corejs.} example : * {core.core.mail() calls corejs/core/mail.js}, type : {library} * @anonymous name : {external} isField : {true}, desc : {Refers to the external libraries.}, type : * {library} * @anonymous name : {db}, isField : {true}, desc : {Refers to the database.}, type : {database} * @anonymous name : {setDB} desc : {changes <tt>db</tt> to refer to a different database.} param : * {type : (string) name : (dbname) desc : (name of the database to which to connect)} * @anonymous name : {SYSOUT} desc : {Prints a string.} param : {type : (string) name : (str) desc : * (the string to print)} * @anonymous name : {log} desc : {Global logger.} param : {type : (string) name : (str) desc : (the * string to log)} * @expose * @docmodule system.system.__instance__ */ public class AppContext extends ServletContextBase implements JSObject, Sizable { /** @unexpose */ static final boolean DEBUG = AppServer.D; /** * If these files exist in the directory or parent directories of a file being run, run these * files first. Includes _init.js and /~~/core/init.js. */ static final String INIT_FILES[] = new String[] {"/~~/core/init.js", "PREFIX_init"}; /** * Initializes a new context for a given site directory. * * @param f the file to run */ public AppContext(File f) { this(f.toString()); } /** * Initializes a new context for a given site's path. * * @param root the path to the site from where ed is being run */ public AppContext(String root) { this(root, guessNameAndEnv(root).name, guessNameAndEnv(root).env); } /** * Initializes a new context. * * @param root the path to the site * @param name the name of the site * @param environment the version of the site */ public AppContext(String root, String name, String environment) { this(root, new File(root), name, environment); } /** * Initializes a new context. * * @param root the path to the site * @param rootFile the directory in which the site resides * @param name the name of the site * @param environment the version of the site */ public AppContext(String root, File rootFile, String name, String environment) { this(root, rootFile, name, environment, null); } private AppContext( String root, File rootFile, String name, String environment, AppContext nonAdminParent) { super(name + ":" + environment); if (root == null) throw new NullPointerException("AppContext root can't be null"); if (rootFile == null) throw new NullPointerException("AppContext rootFile can't be null"); if (name == null) name = guessNameAndEnv(root).name; if (name == null) throw new NullPointerException("how could name be null"); _root = root; _rootFile = rootFile; _git = new GitDir(_rootFile); _name = name; _environment = environment; _nonAdminParent = nonAdminParent; _admin = _nonAdminParent != null; _codePrefix = _admin ? "/~~/modules/admin/" : ""; _moduleRegistry = ModuleRegistry.getNewGlobalChild(); if (_git.isValid()) { _gitBranch = _git.getBranchOrTagName(); _gitHash = _git.getCurrentHash(); } _isGrid = name.equals("grid"); _scope = new Scope( "AppContext:" + root + (_admin ? ":admin" : ""), _isGrid ? ed.cloud.Cloud.getInstance().getScope() : Scope.newGlobal(), null, Language.JS(), _rootFile); _scope.setGlobal(true); _initScope = _scope.child("_init"); _usage = new UsageTracker(this); _baseScopeInit(); _adminContext = _admin ? null : new AppContext(root, rootFile, name, environment, this); _rootContextReachable = new SeenPath(); if (!_admin) _logger.info( "Started Context. root:" + _root + " environment:" + environment + " git branch: " + _gitBranch); } /** * Returns the adapter type for the given file. Will first use the adapter selector function if it * was specified in init.js, otherwise will use the static type (either set in _init file, as a * server-wide override in 10gen.properties, or default of DIRECT_10GEN) * * @param file to produce type for * @return adapter type for the specified file */ public AdapterType getAdapterType(File file) { // Q : I think this is the right thing to do if (inScopeSetup()) { return AdapterType.DIRECT_10GEN; } /* * cheap hack - prevent any _init.* file from getting run as anythign but DIRECT_10GEN */ if (file != null && file.getName().indexOf("_init.") != -1) { return AdapterType.DIRECT_10GEN; } if (_adapterSelector == null) { return _staticAdapterType; } /* * only let the app select type if file is part of application (i.e. * don't do it for corejs, core modules, etc... */ String fp = file.getAbsolutePath(); String fullRoot = _rootFile.getAbsolutePath(); // there must be a nicer way to do this? if (!fp.startsWith(fullRoot)) { return AdapterType.DIRECT_10GEN; } Object o = _adapterSelector.call(_initScope, new JSString(fp.substring(fullRoot.length()))); if (o == null) { return _staticAdapterType; } if (!(o instanceof JSString)) { log("Error : adapter selector not returning string. Ignoring and using static adapter type"); return _staticAdapterType; } AdapterType t = getAdapterTypeFromString(o.toString()); return (t == null ? _staticAdapterType : t); } /** * Creates a copy of this context. * * @return an identical context */ AppContext newCopy() { return new AppContext(_root, _rootFile, _name, _environment, _nonAdminParent); } /** Initializes the base scope for the application */ private void _baseScopeInit() { // --- libraries if (_admin) _scope.put("local", new JSObjectBase(), true); else _setLocalObject(new JSFileLibrary(_rootFile, "local", this)); _loadConfig(); _core = CoreJS.get().getLibrary(getCoreJSVersion(), this, null, true); _logger.info("corejs : " + _core.getRoot()); _scope.put("core", _core, true); _external = Module.getModule("external").getLibrary(getVersionForLibrary("external"), this, null, true); _scope.put("external", _external, true); _scope.put("__instance__", this, true); _scope.lock("__instance__"); // --- db if (!_isGrid) { _scope.put("db", DBProvider.get(this), true); _scope.put( "setDB", new JSFunctionCalls1() { public Object call(Scope s, Object name, Object extra[]) { if (name.equals(_lastSetTo)) return true; DBBase db = (DBBase) AppContext.this._scope.get("db"); if (!db.allowedToAccess(name.toString())) throw new JSException("you are not allowed to access db [" + name + "]"); if (name.equals(db.getName())) return true; AppContext.this._scope.put( "db", DBProvider.get(AppContext.this, name.toString()), false); _lastSetTo = name.toString(); if (_adminContext != null) { // yes, i do want a new copy so Constructors don't get copied for both _adminContext._scope.put( "db", DBProvider.get(AppContext.this, name.toString()), false); } return true; } String _lastSetTo = null; }, true); } // --- output _scope.put( "SYSOUT", new JSFunctionCalls1() { public Object call(Scope s, Object str, Object foo[]) { System.out.println(AppContext.this._name + " \t " + str); return true; } }, true); _scope.put("log", _logger, true); // --- random? _scope.put( "openFile", new JSFunctionCalls1() { public Object call(Scope s, Object name, Object extra[]) { return new JSLocalFile(_rootFile, name.toString()); } }, true); _scope.put("globalHead", _globalHead, true); Map<String, JSFileLibrary> rootFileMap = new HashMap<String, JSFileLibrary>(); for (String rootKey : new String[] {"local", "core", "external"}) { Object temp = _scope.get(rootKey); if (temp instanceof JSFileLibrary) rootFileMap.put(rootKey, (JSFileLibrary) temp); } _scope.put( "fork", new JSFunctionCalls1() { public Object call(final Scope scope, final Object funcJS, final Object extra[]) { if (!(funcJS instanceof JSFunction)) throw new JSException("fork has to take a function"); return queueWork("forked", (JSFunction) funcJS, extra); } }); _scope.lock("fork"); ed.appserver.templates.djang10.JSHelper.install(_scope, rootFileMap, _logger); _scope.lock("user"); // protection against global user object } private void _loadConfig() { try { _configScope.set("__instance__", this); _loadConfigFromCloudObject(getSiteObject()); _loadConfigFromCloudObject(getEnvironmentObject()); File f; if (!_admin) { f = getFileSafe("_config.js"); if (f == null || !f.exists()) f = getFileSafe("_config"); } else f = new File( Module.getModule("core-modules/admin").getRootFile(getVersionForLibrary("admin")), "_config.js"); _libraryLogger.info("config file [" + f + "] exists:" + f.exists()); if (f == null || !f.exists()) return; Convert c = new Convert(f); JSFunction func = c.get(); func.setUsePassedInScope(true); func.call(_configScope); _logger.debug("config things " + _configScope.keySet()); } catch (Exception e) { throw new RuntimeException("couldn't load config", e); } } private void _loadConfigFromCloudObject(JSObject o) { if (o == null) return; _configScope.putAll((JSObject) o.get("config")); } /** * Get the version of corejs to run for this AppContext. * * @return the version of corejs as a string. null if should use default */ public String getCoreJSVersion() { Object o = _scope.get("corejsversion"); if (o != null) { _logger.error("you are using corejsversion which is deprecated. please use version.corejs"); return JS.toString(o); } return getVersionForLibrary("corejs"); } /** * Get the version of a library to run. * * @param name the name of the library to look up * @return the version of the library to run as a string. null if should use default */ public String getVersionForLibrary(String name) { String version = getVersionForLibrary(_configScope, name, this); _libraryVersions.set(name, version); return version; } public JSObject getLibraryVersionsLoaded() { return _libraryVersions; } /** @unexpose */ public static String getVersionForLibrary(Scope s, String name) { AppRequest ar = AppRequest.getThreadLocal(); return getVersionForLibrary(s, name, ar == null ? null : ar.getContext()); } /** @unexpose */ private static String getVersionForLibrary(Scope s, String name, AppContext ctxt) { final String version = _getVersionForLibrary(s, name, ctxt); _libraryLogger.log( ctxt != null && !ctxt._admin ? Level.DEBUG : Level.INFO, ctxt + "\t" + name + "\t" + version); return version; } private static String _getVersionForLibrary(Scope s, String name, AppContext ctxt) { final JSObject o1 = ctxt == null ? null : (JSObject) (s.get("version_" + ctxt.getEnvironmentName())); final JSObject o2 = (JSObject) s.get("version"); _libraryLogger.debug(ctxt + "\t versionConfig:" + (o1 != null) + " config:" + (o2 != null)); String version = _getString(name, o1, o2); if (version != null) return version; if (ctxt == null || ctxt._nonAdminParent == null) return null; return ctxt._nonAdminParent.getVersionForLibrary(name); } private static String _getString(String name, JSObject... places) { for (JSObject o : places) { if (o == null) continue; Object temp = o.get(name); if (temp == null) continue; return temp.toString(); } return null; } /** @return [ <name> , <env> ] */ static NameAndEnv guessNameAndEnv(String root) { root = ed.io.FileUtil.clean(root); root = root.replaceAll("\\.+/", ""); String pcs[] = root.split("/+"); if (pcs.length == 0) throw new RuntimeException("no root for : " + root); // handle anything with sites/foo for (int i = 0; i < pcs.length - 1; i++) if (pcs[i].equals("sites")) { return new NameAndEnv(pcs[i + 1], i + 2 < pcs.length ? pcs[i + 2] : null); } final int start = pcs.length - 1; for (int i = start; i > 0; i--) { String s = pcs[i]; if (i == start && (s.equals("master") || s.equals("test") || s.equals("www") || s.equals("staging") || // s.equals("stage") || s.equals("dev"))) continue; return new NameAndEnv(s, i + 1 < pcs.length ? pcs[i + 1] : null); } return new NameAndEnv(pcs[0], pcs.length > 1 ? pcs[1] : null); } static class NameAndEnv { NameAndEnv(String name, String env) { this.name = name; this.env = env; } final String name; final String env; } /** * Returns the name of the site being run. * * @return the name of the site */ public String getName() { return _name; } /** * Get the database being used. * * @return The database being used */ public DBBase getDB() { return (DBBase) _scope.get("db"); } /** * Given the _id of a JSFile, return the file. * * @param id _id of the file to find * @return The file, if found, otherwise null */ JSFile getJSFile(String id) { if (id == null) return null; DBCollection f = getDB().getCollection("_files"); return (JSFile) (f.find(new ObjectId(id))); } /** * Returns (and if necessary, reinitializes) the scope this context is using. * * @return the scope */ public Scope getScope() { return _scope(); } public Scope getInitScope() { return _initScope; } public Object getInitObject(String what) { return getFromInitScope(what); } public Object getFromInitScope(String what) { if (!_knownInitScopeThings.contains(what)) System.err.println("*** Unknown thing requested from initScope [" + what + "]"); return _initScope.get(what); } public void setInitObject(String name, Object value) { _initScope.set(name, value); } void setTLPreferredScope(AppRequest req, Scope s) { _scope.setTLPreferred(s); } private synchronized Scope _scope() { if (_inScopeSetup) return _scope; if (_getScopeTime() > _lastScopeInitTime) _scopeInited = false; if (_scopeInited) return _scope; _scopeInited = true; _lastScopeInitTime = System.currentTimeMillis(); _setupScope(); _setStaticAdapterType(); _setAdapterSelectorFunction(); return _scope; } protected void _setAdapterSelectorFunction() { Object o = this.getFromInitScope(INIT_ADAPTER_SELECTOR); if (o == null) { log("Adapter selector function not specified in _init file"); return; } if (!(o instanceof JSFunction)) { log( "Adapter selector function specified in _init file not a function. Ignoring. [" + o.getClass() + "]"); return; } _adapterSelector = (JSFunction) o; log("Adapter selector function specified in _init file"); } public void setStaticAdapterTypeValue(AdapterType type) { log("Static adapter type directly set : " + type); _staticAdapterType = type; } public AdapterType getStaticAdapterTypeValue() { return _staticAdapterType; } /** * Figure out what kind of static adapter type was specified. By default it's a 10genDEFAULT app */ protected void _setStaticAdapterType() { /* * app configuration steps could have set this already. If so, don't bother doing anything */ if (_staticAdapterType != AdapterType.UNSET) { log("Static adapter type has already been directly set to " + _staticAdapterType); return; } /* * check to see if overridden in 10gen.properties */ String override = Config.get().getProperty(INIT_ADAPTER_TYPE); if (override != null) { AdapterType t = getAdapterTypeFromString(override); if (t == null) { log( "Static adapter type specified as override [" + override + "] unknown - will use _init file specified or default"); } else { log("Static adapter type overridden by 10gen.properties or env. Value : " + override); _staticAdapterType = t; return; } } /* * if not, use the one from _init file if specified */ _staticAdapterType = AdapterType.DIRECT_10GEN; Object o = getFromInitScope(INIT_ADAPTER_TYPE); if (o == null) { log("Static adapter type not specified in _init file - using default value of DIRECT_10GEN"); return; } if (!(o instanceof JSString)) { log("Static adapter type from _init file not a string - using default value of DIRECT_10GEN"); return; } _staticAdapterType = getAdapterTypeFromString(o.toString()); if (_staticAdapterType == null) { log( "Static adapter type from _init file [" + o.toString() + "] unknown - using default value of DIRECT_10GEN"); _staticAdapterType = AdapterType.DIRECT_10GEN; return; } log("Static adapter type specified in _init file = " + _staticAdapterType); return; } public AdapterType getAdapterTypeFromString(String s) { if (AdapterType.DIRECT_10GEN.toString().equals(s.toUpperCase())) { return AdapterType.DIRECT_10GEN; } if (AdapterType.CGI.toString().equals(s.toUpperCase())) { return AdapterType.CGI; } if (AdapterType.WSGI.toString().equals(s.toUpperCase())) { return AdapterType.WSGI; } return null; } /** @unexpose */ public File getFileSafe(final String uri) { try { return getFile(uri); } catch (FileNotFoundException fnf) { return null; } } /** @unexpose */ public File getFile(final String uri) throws FileNotFoundException { File f = _files.get(uri); if (f != null) return f; if (uri.startsWith("/~~/") || uri.startsWith("~~/")) f = _core.getFileFromPath(uri.substring(3)); else if (uri.startsWith("/admin/")) f = _core.getFileFromPath("/modules" + uri); else if (uri.startsWith("/@@/") || uri.startsWith("@@/")) f = _external.getFileFromPath(uri.substring(3)); else if (_localObject != null && uri.startsWith("/modules/")) f = _localObject.getFileFromPath(uri); else f = new File(_rootFile, uri); if (f == null) throw new FileNotFoundException(uri); _files.put(uri, f); return f; } public String getRealPath(String path) { try { return getFile(path).getAbsolutePath(); } catch (FileNotFoundException fnf) { throw new RuntimeException("file not found [" + path + "]"); } } public URL getResource(String path) { try { File f = getFile(path); if (!f.exists()) return null; return f.toURL(); } catch (FileNotFoundException fnf) { // the spec says to return null if we can't find it // even though this is weird... return null; } catch (IOException ioe) { throw new RuntimeException("error opening [" + path + "]", ioe); } } public InputStream getResourceAsStream(String path) { URL url = getResource(path); if (url == null) return null; try { return url.openStream(); } catch (IOException ioe) { throw new RuntimeException("can't getResourceAsStream [" + path + "]", ioe); } } /** * This causes the AppContext to be started over. All context level variable will be lost. If code * is being managed, will cause it to check that its up to date. */ public void reset() { _reset = true; } /** Checks if this context has been reset. */ public boolean isReset() { return _reset; } /** * Returns the path to the directory the appserver is running. (For example, site/version.) * * @return the path */ public String getRoot() { return _root; } public File getRootFile() { return _rootFile; } /** * Creates an new request for the app server from an HTTP request. * * @param request HTTP request to create * @return the request */ public AppRequest createRequest(HttpRequest request) { return createRequest(request, request.getHost(), request.getURI()); } /** * Creates an new request for the app server from an HTTP request. * * @param request HTTP request to create * @param uri the URI requested * @return the request */ public AppRequest createRequest(HttpRequest request, String host, String uri) { _numRequests++; if (AppRequest.isAdmin(request)) return new AppRequest(_adminContext, request, host, uri); return new AppRequest(this, request, host, uri); } /** * Tries to find the given file, assuming that it's missing the ".jxp" extension * * @param f File to check * @return same file if not found to be missing the .jxp, or a new File w/ the .jxp appended */ File tryNoJXP(File f) { if (f.exists()) return f; if (f.getName().indexOf(".") >= 0) return f; File temp = new File(f.toString() + ".jxp"); return temp.exists() ? temp : f; } File tryOtherExtensions(File f) { if (f.exists()) return f; if (f.getName().indexOf(".") >= 0) return f; for (int i = 0; i < JSFileLibrary._srcExtensions.length; i++) { File temp = new File(f.toString() + JSFileLibrary._srcExtensions[i]); if (temp.exists()) return temp; } return f; } /** * Maps a servlet-like URI to a jxp file. * * @param f File to check * @return new File with <root>.jxp if exists, orig file if not * @example /wiki/geir -> maps to wiki.jxp if exists */ File tryServlet(File f) { if (f.exists()) return f; String uri = f.toString(); if (uri.startsWith(_rootFile.toString())) uri = uri.substring(_rootFile.toString().length()); if (_core != null && uri.startsWith(_core._base.toString())) uri = "/~~" + uri.substring(_core._base.toString().length()); while (uri.startsWith("/")) uri = uri.substring(1); int start = 0; while (true) { int idx = uri.indexOf("/", start); if (idx < 0) break; String foo = uri.substring(0, idx); File temp = getFileSafe(foo + ".jxp"); if (temp != null && temp.exists()) f = temp; start = idx + 1; } return f; } /** * Returns the index.jxp for the File argument if it's an existing directory, and the index.jxp * file exists * * @param f directory to check * @return new File for index.jxp in that directory, or same file object if not */ File tryIndex(File f) { if (!(f.isDirectory() && f.exists())) return f; for (int i = 0; i < JSFileLibrary._srcExtensions.length; i++) { File temp = new File(f, "index" + JSFileLibrary._srcExtensions[i]); if (temp.exists()) return temp; } return f; } JxpSource getSource(File f) throws IOException { if (DEBUG) System.err.println("getSource\n\t " + f); File temp = _findFile(f); if (DEBUG) System.err.println("\t " + temp); if (!temp.exists()) return handleFileNotFound(f); // if it's a directory (and we know we can't find the index file) // TODO : at some point, do something where we return an index for the dir? if (temp.isDirectory()) return null; // if we are at init time, save it as an initializaiton file loadedFile(temp); // Ensure that this is w/in the right tree for the context if (_localObject != null && _localObject.isIn(temp)) return _localObject.getSource(temp); // if not, is it core? if (_core.isIn(temp)) return _core.getSource(temp); throw new RuntimeException("what? can't find:" + f); } /** * Finds the appropriate file for the given path. * * <p>We have a hierarchy of attempts as we try to find a file : * * <p>1) first, see if it exists as is, or if it's really a .jxp w/o the extension 2) next, see if * it can be deconstructed as a servlet such that /foo/bar maps to /foo.jxp 3) See if we can find * the index file for it if a directory */ File _findFile(File f) { File temp; if ((temp = tryNoJXP(f)) != f) { return temp; } if ((temp = tryOtherExtensions(f)) != f) { return temp; } if ((temp = tryServlet(f)) != f) { return temp; } if ((temp = tryIndex(f)) != f) { return temp; } return f; } public void loadedFile(File f) { if (_inScopeSetup) _initFlies.add(f); } public void addInitDependency(File f) { _initFlies.add(f); } JxpServlet getServlet(File f) throws IOException { // if this site doesn't exist, don't return anything if (!_rootFile.exists()) return null; JxpSource source = getSource(f); if (source == null) return null; return source.getServlet(this); } private void _setupScope() { if (_inScopeSetup) return; final Scope saveTLPref = _scope.getTLPreferred(); _scope.setTLPreferred(null); final Scope saveTL = Scope.getThreadLocal(); _scope.makeThreadLocal(); _inScopeSetup = true; try { Object fo = getConfigObject("framework"); if (fo != null) { Framework f = null; if (fo instanceof JSString) { f = Framework.byName(fo.toString(), null); // we allow people to just specify name _logger.info("Setting framework by name [" + fo.toString() + "]"); } else if (fo instanceof JSObjectBase) { JSObjectBase obj = (JSObjectBase) fo; if (obj.containsKey("name")) { String s = obj.getAsString("version"); f = Framework.byName(obj.getAsString("name"), s); _logger.info("Setting framework by name [" + obj.getAsString("name") + "]"); } else if (obj.containsKey("custom")) { Object o = obj.get("custom"); if (o instanceof JSObjectBase) { f = Framework.byCustom((JSObjectBase) o); _logger.info("Setting framework by custom [" + o + "]"); } else { throw new RuntimeException("Error - custom framework wasn't an object [" + o + "]"); } } else if (obj.containsKey("classname")) { f = Framework.byClass(obj.getAsString("classname")); _logger.info("Setting framework by class [" + obj.getAsString("classname") + "]"); } } if (f == null) { throw new RuntimeException("Error : can't find framework [" + fo + "]"); } f.install(this); } _runInitFiles(INIT_FILES); if (_adminContext != null) { _adminContext._scope.set("siteScope", _scope); _adminContext._setLocalObject(_localObject); } _lastScopeInitTime = _getScopeTime(); } catch (RuntimeException re) { _scopeInited = false; throw re; } catch (Exception e) { _scopeInited = false; throw new RuntimeException(e); } finally { _inScopeSetup = false; _scope.setTLPreferred(saveTLPref); if (saveTL != null) saveTL.makeThreadLocal(); this.approxSize(_rootContextReachable); } } public boolean inScopeSetup() { return _inScopeSetup; } private void _runInitFiles(String[] files) throws IOException { if (files == null) return; for (JSFunction func : _initRefreshHooks) { func.call(_initScope, null); } for (int i = 0; i < files.length; i++) runInitFile(files[i].replaceAll("PREFIX", _codePrefix)); } public void addInitRefreshHook(JSFunction func) { _initRefreshHooks.add(func); } /** @param path (ex: /~~/foo.js ) */ public void runInitFile(String path) throws IOException { _runInitFile(tryOtherExtensions(getFile(path))); } private void _runInitFile(File f) throws IOException { if (f == null) return; if (!f.exists()) return; _initFlies.add(f); JxpSource s = getSource(f); JSFunction func = s.getFunction(); func.setUsePassedInScope(true); func.call(_initScope); } long _getScopeTime() { long last = 0; for (File f : _initFlies) if (f.exists()) last = Math.max(last, f.lastModified()); return last; } /** * Convert this AppContext to a string by returning the name of the directory it's running in. * * @return the filename of its root directory */ public String toString() { return _rootFile.toString(); } public String debugInfo() { return _rootFile + " admin:" + _admin; } public void fix(Throwable t) { StackTraceHolder.getInstance().fix(t); } /** * Get a "global" head array. This array contains HTML that will be inserted into the head of * every request served by this app context. It's analagous to the <tt>head</tt> array, but * persistent. * * @return a mutable array */ public JSArray getGlobalHead() { return _globalHead; } /** * Gets the date of creation for this app context. * * @return the creation date as a JS Date. */ public JSDate getWhenCreated() { return _created; } /** * Gets the number of requests served by this app context. * * @return the number of requests served */ public int getNumRequests() { return _numRequests; } /** * Get the name of the git branch we think we're running. * * @return the name of the git branch, as a string */ public String getGitBranch() { return _gitBranch; } public String getGitHash() { return _gitHash; } /** * Update the git branch that we're running and return it. * * @return the name of the git branch, or null if there isn't any */ public String getCurrentGitBranch() { return getCurrentGitBranch(false); } public String getCurrentGitBranch(boolean forceUpdate) { if (_gitBranch == null) return null; if (_gitFile == null) _gitFile = new File(_rootFile, ".git/HEAD"); if (!_gitFile.exists()) throw new RuntimeException("this should be impossible"); if (forceUpdate || _lastScopeInitTime < _gitFile.lastModified()) { _gitBranch = _git.getBranchOrTagName(); _gitHash = _git.getCurrentHash(); } return _gitBranch; } /** * Get the environment in which this site is running * * @return the environment name as a string */ public String getEnvironmentName() { return _environment; } /** * updates the context to the correct branch based on environment and to the latest version of the * code if name or environemnt is missing, does nothing */ public String updateCode() { if (!_git.isValid()) throw new RuntimeException(_rootFile + " is not a git repository"); _logger.info("going to update code"); _git.fullUpdate(); if (_name == null || _environment == null) return getCurrentGitBranch(); JSObject env = getEnvironmentObject(); if (env == null) return null; String branch = env.get("branch").toString(); _logger.info("updating to [" + branch + "]"); _git.checkout(branch); Python.deleteCachedJythonFiles(_rootFile); return getCurrentGitBranch(true); } private JSObject getSiteObject() { return AppContextHolder.getSiteFromCloud(_name); } private JSObject getEnvironmentObject() { return AppContextHolder.getEnvironmentFromCloud(_name, _environment); } private void _setLocalObject(JSFileLibrary local) { _localObject = local; _scope.put("local", _localObject, true); _scope.put("jxp", _localObject, true); _scope.warn("jxp"); } JxpSource handleFileNotFound(File f) { String name = f.getName(); if (name.endsWith(".class")) { name = name.substring(0, name.length() - 6); return getJxpServlet(name); } return null; } public JxpSource getJxpServlet(String name) { JxpSource source = _httpServlets.get(name); if (source != null) return source; try { Class c = Class.forName(name); Object n = c.newInstance(); if (!(n instanceof HttpServlet)) throw new RuntimeException("class [" + name + "] is not a HttpServlet"); HttpServlet servlet = (HttpServlet) n; servlet.init(createServletConfig(name)); source = new ServletSource(servlet); _httpServlets.put(name, source); return source; } catch (Exception e) { throw new RuntimeException("can't load [" + name + "]", e); } } ServletConfig createServletConfig(final String name) { final Object rawServletConfigs = _scope.get("servletConfigs"); final Object servletConfigObject = rawServletConfigs instanceof JSObject ? ((JSObject) rawServletConfigs).get(name) : null; final JSObject servletConfig; if (servletConfigObject instanceof JSObject) servletConfig = (JSObject) servletConfigObject; else servletConfig = null; return new ServletConfig() { public String getInitParameter(String name) { if (servletConfig == null) return null; Object foo = servletConfig.get(name); if (foo == null) return null; return foo.toString(); } public Enumeration getInitParameterNames() { Collection keys; if (servletConfig == null) keys = new LinkedList(); else keys = servletConfig.keySet(); return new CollectionEnumeration(keys); } public ServletContext getServletContext() { return AppContext.this; } public String getServletName() { return name; } }; } public static AppContext findThreadLocal() { AppContext context = _tl.get(); if (context != null) return context; AppRequest req = AppRequest.getThreadLocal(); if (req != null) return req._context; Scope s = Scope.getThreadLocal(); if (s != null) { Object foo = s.get("__instance__"); if (foo instanceof AppContext) return (AppContext) foo; } return null; } public void makeThreadLocal() { _tl.set(this); } public static void clearThreadLocal() { _tl.set(null); } public String getInitParameter(String name) { Object foo = _configScope.get(name); if (foo == null) return null; return foo.toString(); } public Enumeration getInitParameterNames() { return new CollectionEnumeration(_configScope.keySet()); } public Object getConfigObject(String name) { return _configScope.get(name); } public void setConfigObject(String name, Object value) { _configScope.set(name, value); } public String getContextPath() { return ""; } public RequestDispatcher getNamedDispatcher(String name) { throw new RuntimeException("getNamedDispatcher not implemented"); } public RequestDispatcher getRequestDispatcher(String name) { throw new RuntimeException("getRequestDispatcher not implemented"); } public Set getResourcePaths(String path) { throw new RuntimeException("getResourcePaths not implemented"); } public AppContext getSiteInstance() { if (_nonAdminParent == null) return this; return _nonAdminParent; } public long approxSize() { return approxSize(new SeenPath()); } public long approxSize(SeenPath seen) { long size = 0; seen.visited(this); if (seen.shouldVisit(_scope, this)) size += _scope.approxSize(seen, false, true); if (seen.shouldVisit(_initScope, this)) size += _initScope.approxSize(seen, true, false); size += JSObjectSize.size(_localObject, seen, this); size += JSObjectSize.size(_core, seen, this); size += JSObjectSize.size(_external, seen, this); if (seen.shouldVisit(_adminContext, this)) size += _adminContext.approxSize(seen); size += JSObjectSize.size(_logger, seen, this); return size; } public int hashCode() { return System.identityHashCode(this); } public boolean equals(Object o) { return o == this; } public AppWork queueWork(String identifier, JSFunction work, Object... params) { return queueWork(new AppWork.FunctionAppWork(this, identifier, work, params)); } public AppWork queueWork(AppWork work) { if (_workQueue == null) { _workQueue = new ArrayBlockingQueue<AppWork>(100); AppWork.addQueue(_workQueue); } if (_workQueue.offer(work)) return work; throw new RuntimeException("work queue full!"); } public Logger getLogger(String sub) { return _logger.getChild(sub); } public ModuleRegistry getModuleRegistry() { return _moduleRegistry; } // ---- START JSObject INTERFACE public Object get(Object n) { return _scope.get(n); } public JSFunction getFunction(String name) { return _scope.getFunction(name); } public final Set<String> keySet() { return _scope.keySet(); } public Set<String> keySet(boolean includePrototype) { return _scope.keySet(includePrototype); } public boolean containsKey(String s) { return _scope.containsKey(s); } public boolean containsKey(String s, boolean includePrototype) { return _scope.containsKey(s, includePrototype); } public Object set(Object n, Object v) { return _scope.putExplicit(n.toString(), v); } public Object setInt(int n, Object v) { throw new RuntimeException("not allowed"); } public Object getInt(int n) { return _scope.getInt(n); } public Object removeField(Object n) { return _scope.removeField(n); } public JSFunction getConstructor() { return null; } public JSObject getSuper() { return null; } // ---- END BROKEN JSOBJET INTERFACE public TimeZone getTimeZone() { return _tz; } public void setTimeZone(String tz) { if (tz.length() == 3) tz = tz.toUpperCase(); _tz = TimeZone.getTimeZone(tz); if (!_tz.getID().equals(tz)) throw new RuntimeException("can't find time zone[" + tz + "]"); } final String _name; final String _root; final File _rootFile; final GitDir _git; private String _gitBranch; private String _gitHash; final String _environment; final boolean _admin; final AppContext _adminContext; final String _codePrefix; final AppContext _nonAdminParent; private JSFileLibrary _localObject; private JSFileLibrary _core; private JSFileLibrary _external; final Scope _scope; final SeenPath _rootContextReachable; final Scope _initScope; final Scope _configScope = new Scope(); final UsageTracker _usage; final ModuleRegistry _moduleRegistry; final JSArray _globalHead = new JSArray(); private final Map<String, File> _files = Collections.synchronizedMap(new HashMap<String, File>()); private final Set<File> _initFlies = new HashSet<File>(); private final Map<String, JxpSource> _httpServlets = Collections.synchronizedMap(new HashMap<String, JxpSource>()); private final JSObject _libraryVersions = new JSObjectBase(); private Queue<AppWork> _workQueue; private TimeZone _tz = TimeZone.getDefault(); boolean _scopeInited = false; boolean _inScopeSetup = false; long _lastScopeInitTime = 0; final boolean _isGrid; boolean _reset = false; int _numRequests = 0; final JSDate _created = new JSDate(); private File _gitFile = null; private long _lastGitCheckTime = 0; private Collection<JSFunction> _initRefreshHooks = new ArrayList<JSFunction>(); /* * adapter type - can have either a static ("all files in this app are X") * or dynamic - the provided selector function dynamically chooses, falling * back to the static if it returns null */ public static final String INIT_ADAPTER_TYPE = "adapterType"; public static final String INIT_ADAPTER_SELECTOR = "adapterSelector"; private AdapterType _staticAdapterType = AdapterType.UNSET; private JSFunction _adapterSelector = null; private static Logger _libraryLogger = Logger.getLogger("library.load"); static { _libraryLogger.setLevel(Level.INFO); } private static final Set<String> _knownInitScopeThings = new HashSet<String>(); private static final ThreadLocal<AppContext> _tl = new ThreadLocal<AppContext>(); static { _knownInitScopeThings.add("mapUrlToJxpFileCore"); _knownInitScopeThings.add("mapUrlToJxpFile"); _knownInitScopeThings.add("allowed"); _knownInitScopeThings.add("staticCacheTime"); _knownInitScopeThings.add("handle404"); _knownInitScopeThings.add(INIT_ADAPTER_TYPE); _knownInitScopeThings.add(INIT_ADAPTER_SELECTOR); } public static final class AppContextReachable extends ReflectionVisitor.Reachable { public boolean follow(Object o, Class c, java.lang.reflect.Field f) { if (_reachableStoppers.contains(c)) return false; if (f != null && _reachableStoppers.contains(f.getType())) return false; return super.follow(o, c, f); } } private static final Set<Class> _reachableStoppers = new HashSet<Class>(); static { _reachableStoppers.add(HttpServer.class); _reachableStoppers.add(AppServer.class); _reachableStoppers.add(DBTCP.class); _reachableStoppers.add(Mongo.class); _reachableStoppers.add(WeakBag.class); _reachableStoppers.add(WeakValueMap.class); } }
public class FileInputStreamReadBytesBug { private static final Logger logger = Logger.getLogger(FileInputStreamReadBytesBug.class.getName()); public static void main(String[] args) throws Exception { int counter = 0; while (true) { Thread outThread = null; Thread errThread = null; try { // org.pitest.mutationtest.instrument.MutationTestUnit#runTestInSeperateProcessForMutationRange // *** start slave ServerSocket commSocket = new ServerSocket(0); int commPort = commSocket.getLocalPort(); System.out.println("commPort = " + commPort); // org.pitest.mutationtest.execute.MutationTestProcess#start // - org.pitest.util.CommunicationThread#start FutureTask<Integer> commFuture = createFuture(commSocket); // - org.pitest.util.WrappingProcess#start // - org.pitest.util.JavaProcess#launch Process slaveProcess = startSlaveProcess(commPort); outThread = new Thread(new ReadFromInputStream(slaveProcess.getInputStream()), "stdout"); errThread = new Thread(new ReadFromInputStream(slaveProcess.getErrorStream()), "stderr"); outThread.start(); errThread.start(); // *** wait for slave to die // org.pitest.mutationtest.execute.MutationTestProcess#waitToDie // - org.pitest.util.CommunicationThread#waitToFinish System.out.println("waitToFinish"); Integer controlReturned = commFuture.get(); System.out.println("controlReturned = " + controlReturned); // NOTE: the following won't get called if commFuture.get() fails! // - org.pitest.util.JavaProcess#destroy outThread.interrupt(); // org.pitest.util.AbstractMonitor#requestStop errThread.interrupt(); // org.pitest.util.AbstractMonitor#requestStop slaveProcess.destroy(); } catch (Exception e) { e.printStackTrace(System.out); } // test: the threads should exit eventually outThread.join(); errThread.join(); counter++; System.out.println("try " + counter + ": stdout and stderr threads exited normally"); } } private static Process startSlaveProcess(int commPort) throws IOException { String separator = System.getProperty("file.separator"); String javaProc = System.getProperty("java.home") + separator + "bin" + separator + "java"; ProcessBuilder pb = new ProcessBuilder( javaProc, "-cp", "target/test-classes;.", CrashingSlave.class.getName(), String.valueOf(commPort)); return pb.start(); } private static FutureTask<Integer> createFuture(final ServerSocket controlSocket) { // org.pitest.util.CommunicationThread#createFuture FutureTask<Integer> commFuture = new FutureTask<Integer>( new Callable<Integer>() { @Override public Integer call() throws Exception { // org.pitest.util.SocketReadingCallable#call Socket clientSocket = controlSocket.accept(); try { InputStream in = clientSocket.getInputStream(); int b; while ((b = in.read()) > 0) { System.out.println("control read " + b); } in.close(); return b; } finally { clientSocket.close(); controlSocket.close(); } } }); new Thread(commFuture, "communication").start(); return commFuture; } private static class ReadFromInputStream implements Runnable { private final InputStream in; public ReadFromInputStream(InputStream in) { this.in = in; } @Override public void run() { byte[] buf = new byte[100]; try { int len; while ((len = in.read(buf)) > 0) { String output = new String(buf, 0, len); Thread t = Thread.currentThread(); System.out.println( "thread " + t.getName() + " " + t.getId() + ", read " + len + " bytes: " + output); } } catch (IOException e) { logger.log(Level.SEVERE, "Failed to read", e); } finally { try { in.close(); } catch (IOException e) { logger.log(Level.SEVERE, "Failed to close", e); } } } } }
/** * A reactor that selects on some stuff and then notifies some Communicators that things happened */ public class Overlord { private Selector selector; private Pipe pipe; private static final Logger log = Logger.getLogger("Overlord"); private ConcurrentLinkedQueue<Communicator> queue; // This is just used to read the one byte off of pipes informing us that // there is data on some queue. ByteBuffer ignored = ByteBuffer.allocate(10); public Overlord() { try { selector = Selector.open(); queue = new ConcurrentLinkedQueue<Communicator>(); // open the pipe and register it with our selector pipe = Pipe.open(); pipe.sink().configureBlocking(false); pipe.source().configureBlocking(false); pipe.source().register(selector, SelectionKey.OP_READ); } catch (IOException e) { throw new RuntimeException("select() failed"); } } /** Selects on sockets and informs their Communicator when there is something to do. */ public void communicate(int timeout) { try { selector.select(timeout); } catch (IOException e) { // Not really sure why/when this happens yet return; } Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey key = keys.next(); keys.remove(); if (!key.isValid()) continue; // WHY Communicator communicator = (Communicator) key.attachment(); if (key.isReadable()) communicator.onReadable(); if (key.isWritable()) communicator.onWritable(); if (key.isAcceptable()) communicator.onAcceptable(); } // Go through the queue and handle each communicator while (!queue.isEmpty()) { Communicator c = queue.poll(); c.onMemo(); } } public void offer(Communicator c) { queue.offer(c); } /** Registers a SelectableChannel */ public boolean register(SelectableChannel sc, Communicator communicator) { try { sc.register(selector, sc.validOps(), communicator); return true; } catch (Exception e) { return false; } } /** If the selector is waiting, wake it up */ public void interrupt() { selector.wakeup(); } /** Registers a SelectableQueue */ public boolean register(SelectableQueue sq, Communicator communicator) { try { // Register the new pipe with the queue. It will write a byte to this // pipe when the queue is hot, and it will offer its communicator to our // queue. sq.register(this, communicator); return true; } catch (Exception e) { e.printStackTrace(); return false; } } }
public class JungGraphObserver implements Control, HyphaDataListener, HyphaLinkListener { private static final String PAR_HYPHADATA_PROTO = "network.node.hyphadata_proto"; private static final String PAR_HYPHALINK_PROTO = "network.node.hyphalink_proto"; private static final String PAR_MYCOCAST_PROTO = "network.node.mycocast_proto"; private static final String PAR_WALK_DELAY = "walk_delay"; private static Logger log = Logger.getLogger(JungGraphObserver.class.getName()); private static final String PAR_PERIOD = "period"; private static int period; private static int walkDelay; private final String name; private final int hyphadataPid; private final int hyphalinkPid; private final int mycocastPid; // private static Lock = new ReentrantLock(); private static MycoGraph graph = new MycoGraph(); private static VisualizationViewer<MycoNode, MycoEdge> visualizer; private static Set<ChangeListener> changeListeners; // private class TypePredicate extends Predicate<{ // } public static void addChangeListener(ChangeListener cl) { changeListeners.add(cl); } public static void removeChangeListener(ChangeListener cl) { if (changeListeners.contains(cl)) { changeListeners.remove(cl); } } private static Thread mainThread; public static boolean stepBlocked = false; public static boolean noBlock = true; public static boolean walking = false; public JungGraphObserver(String name) { this.name = name; this.hyphadataPid = Configuration.getPid(PAR_HYPHADATA_PROTO); this.hyphalinkPid = Configuration.getPid(PAR_HYPHALINK_PROTO); this.mycocastPid = Configuration.getPid(PAR_MYCOCAST_PROTO); this.period = Configuration.getInt(name + "." + PAR_PERIOD); this.walkDelay = Configuration.getInt(name + "." + PAR_WALK_DELAY); mainThread = Thread.currentThread(); this.changeListeners = new HashSet<ChangeListener>(); visualizer = null; // HyphaData.addHyphaDataListener(this); // HyphaLink.addHyphaLinkListener(this); } public static void setVisualizer(VisualizationViewer<MycoNode, MycoEdge> visualizer) { JungGraphObserver.visualizer = visualizer; addChangeListener(visualizer); } public static MycoGraph getGraph() { return graph; } public void nodeStateChanged(MycoNode n, HyphaType t, HyphaType oldState) { if (t != HyphaType.DEAD) { if (!graph.containsVertex(n)) { graph.addVertex(n); } } else { if (graph.containsVertex(n)) { graph.removeVertex(n); } } } public void linkAdded(MycoNode a, MycoNode b) { if (graph.findEdge(a, b) == null) { MycoEdge edge = new MycoEdge(); graph.addEdge(edge, a, b, EdgeType.DIRECTED); } } public void linkRemoved(MycoNode a, MycoNode b) { MycoEdge edge = graph.findEdge(a, b); while (edge != null) { graph.removeEdge(edge); edge = graph.findEdge(a, b); } } public boolean execute() { if (CDState.getCycle() % period != 0) return false; MycoCast mycocast = (MycoCast) Network.get(0).getProtocol(mycocastPid); int bio = mycocast.countBiomass(); int ext = mycocast.countExtending(); int bra = mycocast.countBranching(); int imm = mycocast.countImmobile(); // Update vertices Set<MycoNode> activeNodes = new HashSet<MycoNode>(); for (int i = 0; i < Network.size(); i++) { MycoNode n = (MycoNode) Network.get(i); activeNodes.add(n); HyphaData data = n.getHyphaData(); // if (data.isBiomass()) { continue; } if (graph.containsVertex(n)) { graph.removeVertex(n); } if (!graph.containsVertex(n)) { graph.addVertex(n); } } Set<MycoNode> jungNodes = new HashSet<MycoNode>(graph.getVertices()); jungNodes.removeAll(activeNodes); for (MycoNode n : jungNodes) { graph.removeVertex(n); } // Update edges for (int i = 0; i < Network.size(); i++) { MycoNode n = (MycoNode) Network.get(i); HyphaData data = n.getHyphaData(); HyphaLink link = n.getHyphaLink(); synchronized (graph) { // We now add in all links and tune out display in Visualizer java.util.List<MycoNode> neighbors = (java.util.List<MycoNode>) link.getNeighbors(); //// Adding only links to hypha thins out links to biomass // (java.util.List<MycoNode>) link.getHyphae(); Collection<MycoNode> jungNeighbors = graph.getNeighbors(n); // Remove edges from Jung graph that are not in peersim graph for (MycoNode o : jungNeighbors) { if (!neighbors.contains(o)) { MycoEdge edge = graph.findEdge(n, o); while (edge != null) { graph.removeEdge(edge); edge = graph.findEdge(n, o); } } } // Add missing edges to Jung graph that are in peersim graph for (MycoNode o : neighbors) { if (graph.findEdge(n, o) == null) { MycoEdge edge = new MycoEdge(); graph.addEdge(edge, n, o, EdgeType.DIRECTED); } } } // log.finest("VERTICES: " + graph.getVertices()); // log.finest("EDGES: " + graph.getEdges()); } for (ChangeListener cl : changeListeners) { cl.stateChanged(new ChangeEvent(graph)); } if (walking) { try { Thread.sleep(walkDelay); } catch (InterruptedException e) { } stepBlocked = false; } try { while (stepBlocked && !noBlock) { synchronized (JungGraphObserver.class) { JungGraphObserver.class.wait(); } } } catch (InterruptedException e) { stepBlocked = true; } stepBlocked = true; // System.out.println(graph.toString()); return false; } public static synchronized void stepAction() { walking = false; stepBlocked = false; noBlock = false; JungGraphObserver.class.notifyAll(); } public static synchronized void walkAction() { walking = true; stepBlocked = false; noBlock = false; JungGraphObserver.class.notifyAll(); } public static synchronized void pauseAction() { walking = false; stepBlocked = true; noBlock = false; } public static synchronized void runAction() { walking = false; stepBlocked = false; noBlock = true; JungGraphObserver.class.notifyAll(); } }
/** * Keeps track of entity capabilities. * * <p>This work is based on Jonas Adahl's smack fork. * * @author Emil Ivov * @author Lyubomir Marinov */ public class EntityCapsManager { /** * The <tt>Logger</tt> used by the <tt>EntityCapsManager</tt> class and its instances for logging * output. */ private static final Logger logger = Logger.getLogger(EntityCapsManager.class); /** Static OSGi bundle context used by this class. */ private static BundleContext bundleContext; /** Configuration service instance used by this class. */ private static ConfigurationService configService; /** * The prefix of the <tt>ConfigurationService</tt> properties which persist {@link * #caps2discoverInfo}. */ private static final String CAPS_PROPERTY_NAME_PREFIX = "net.java.sip.communicator.impl.protocol.jabber.extensions.caps." + "EntityCapsManager.CAPS."; /** * An empty array of <tt>UserCapsNodeListener</tt> elements explicitly defined in order to reduce * unnecessary allocations. */ private static final UserCapsNodeListener[] NO_USER_CAPS_NODE_LISTENERS = new UserCapsNodeListener[0]; /** The node value to advertise. */ private static String entityNode = OSUtils.IS_ANDROID ? "http://android.jitsi.org" : "http://jitsi.org"; /** * The <tt>Map</tt> of <tt>Caps</tt> to <tt>DiscoverInfo</tt> which associates a node#ver with the * entity capabilities so that they don't have to be retrieved every time their necessary. Because * ver is constructed from the entity capabilities using a specific hash method, the hash method * is also associated with the entity capabilities along with the node and the ver in order to * disambiguate cases of equal ver values for different entity capabilities constructed using * different hash methods. */ private static final Map<Caps, DiscoverInfo> caps2discoverInfo = new ConcurrentHashMap<Caps, DiscoverInfo>(); /** * Map of Full JID -> DiscoverInfo/null. In case of c2s connection the key is formed as * user@server/resource (resource is required) In case of link-local connection the key is formed * as user@host (no resource) */ private final Map<String, Caps> userCaps = new ConcurrentHashMap<String, Caps>(); /** CapsVerListeners gets notified when the version string is changed. */ private final Set<CapsVerListener> capsVerListeners = new CopyOnWriteArraySet<CapsVerListener>(); /** The current hash of our version and supported features. */ private String currentCapsVersion = null; /** * The list of <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the * list of user caps nodes of this <tt>EntityCapsManager</tt>. */ private final List<UserCapsNodeListener> userCapsNodeListeners = new LinkedList<UserCapsNodeListener>(); static { ProviderManager.getInstance() .addExtensionProvider( CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE, new CapsProvider()); } /** * Add {@link DiscoverInfo} to our caps database. * * <p><b>Warning</b>: The specified <tt>DiscoverInfo</tt> is trusted to be valid with respect to * the specified <tt>Caps</tt> for performance reasons because the <tt>DiscoverInfo</tt> should * have already been validated in order to be used elsewhere anyway. * * @param caps the <tt>Caps<tt/> i.e. the node, the hash and the ver for which a * <tt>DiscoverInfo</tt> is to be added to our caps database. * @param info {@link DiscoverInfo} for the specified <tt>Caps</tt>. */ public static void addDiscoverInfoByCaps(Caps caps, DiscoverInfo info) { cleanupDiscoverInfo(info); /* * DiscoverInfo carries the node we're now associating it with a * specific node so we'd better keep them in sync. */ info.setNode(caps.getNodeVer()); synchronized (caps2discoverInfo) { DiscoverInfo oldInfo = caps2discoverInfo.put(caps, info); /* * If the specified info is a new association for the specified * node, remember it across application instances in order to not * query for it over the network. */ if ((oldInfo == null) || !oldInfo.equals(info)) { String xml = info.getChildElementXML(); if ((xml != null) && (xml.length() != 0)) { getConfigService().setProperty(getCapsPropertyName(caps), xml); } } } } /** * Gets the name of the property in the <tt>ConfigurationService</tt> which is or is to be * associated with a specific <tt>Caps</tt> value. * * @param caps the <tt>Caps</tt> value for which the associated <tt>ConfigurationService</tt> * property name is to be returned * @return the name of the property in the <tt>ConfigurationService</tt> which is or is to be * associated with a specific <tt>Caps</tt> value */ private static String getCapsPropertyName(Caps caps) { return CAPS_PROPERTY_NAME_PREFIX + caps.node + '#' + caps.hash + '#' + caps.ver; } /** Returns cached instance of {@link ConfigurationService}. */ private static ConfigurationService getConfigService() { if (configService == null) { configService = ServiceUtils.getService(bundleContext, ConfigurationService.class); } return configService; } /** * Sets OSGi bundle context instance that will be used by this class. * * @param bundleContext the <tt>BundleContext</tt> instance to be used by this class or * <tt>null</tt> to clear the reference. */ public static void setBundleContext(BundleContext bundleContext) { if (bundleContext == null) { configService = null; } EntityCapsManager.bundleContext = bundleContext; } /** * Add a record telling what entity caps node a user has. * * @param user the user (Full JID) * @param node the node (of the caps packet extension) * @param hash the hashing algorithm used to calculate <tt>ver</tt> * @param ver the version (of the caps packet extension) * @param ext the ext (of the caps packet extension) * @param online indicates if the user is online */ private void addUserCapsNode( String user, String node, String hash, String ver, String ext, boolean online) { if ((user != null) && (node != null) && (hash != null) && (ver != null)) { Caps caps = userCaps.get(user); if ((caps == null) || !caps.node.equals(node) || !caps.hash.equals(hash) || !caps.ver.equals(ver)) { caps = new Caps(node, hash, ver, ext); userCaps.put(user, caps); } else return; // Fire userCapsNodeAdded. UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeAdded(user, nodeVer, online); } } } /** * Adds a specific <tt>UserCapsNodeListener</tt> to the list of <tt>UserCapsNodeListener</tt>s * interested in events notifying about changes in the list of user caps nodes of this * <tt>EntityCapsManager</tt>. * * @param listener the <tt>UserCapsNodeListener</tt> which is interested in events notifying about * changes in the list of user caps nodes of this <tt>EntityCapsManager</tt> */ public void addUserCapsNodeListener(UserCapsNodeListener listener) { if (listener == null) throw new NullPointerException("listener"); synchronized (userCapsNodeListeners) { if (!userCapsNodeListeners.contains(listener)) userCapsNodeListeners.add(listener); } } /** * Remove records telling what entity caps node a contact has. * * @param contact the contact */ public void removeContactCapsNode(Contact contact) { Caps caps = null; String lastRemovedJid = null; Iterator<String> iter = userCaps.keySet().iterator(); while (iter.hasNext()) { String jid = iter.next(); if (StringUtils.parseBareAddress(jid).equals(contact.getAddress())) { caps = userCaps.get(jid); lastRemovedJid = jid; iter.remove(); } } // fire only for the last one, at the end the event out // of the protocol will be one and for the contact if (caps != null) { UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeRemoved(lastRemovedJid, nodeVer, false); } } } /** * Remove a record telling what entity caps node a user has. * * @param user the user (Full JID) */ public void removeUserCapsNode(String user) { Caps caps = userCaps.remove(user); // Fire userCapsNodeRemoved. if (caps != null) { UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeRemoved(user, nodeVer, false); } } } /** * Removes a specific <tt>UserCapsNodeListener</tt> from the list of * <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the list of user * caps nodes of this <tt>EntityCapsManager</tt>. * * @param listener the <tt>UserCapsNodeListener</tt> which is no longer interested in events * notifying about changes in the list of user caps nodes of this <tt>EntityCapsManager</tt> */ public void removeUserCapsNodeListener(UserCapsNodeListener listener) { if (listener != null) { synchronized (userCapsNodeListeners) { userCapsNodeListeners.remove(listener); } } } /** * Gets the <tt>Caps</tt> i.e. the node, the hash and the ver of a user. * * @param user the user (Full JID) * @return the <tt>Caps</tt> i.e. the node, the hash and the ver of <tt>user</tt> */ public Caps getCapsByUser(String user) { return userCaps.get(user); } /** * Get the discover info given a user name. The discover info is returned if the user has a * node#ver associated with it and the node#ver has a discover info associated with it. * * @param user user name (Full JID) * @return the discovered info */ public DiscoverInfo getDiscoverInfoByUser(String user) { Caps caps = userCaps.get(user); return (caps == null) ? null : getDiscoverInfoByCaps(caps); } /** * Get our own caps version. * * @return our own caps version */ public String getCapsVersion() { return currentCapsVersion; } /** * Get our own entity node. * * @return our own entity node. */ public String getNode() { return entityNode; } /** * Set our own entity node. * * @param node the new node */ public void setNode(String node) { entityNode = node; } /** * Retrieve DiscoverInfo for a specific node. * * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver * @return The corresponding DiscoverInfo or null if none is known. */ public static DiscoverInfo getDiscoverInfoByCaps(Caps caps) { synchronized (caps2discoverInfo) { DiscoverInfo discoverInfo = caps2discoverInfo.get(caps); /* * If we don't have the discoverInfo in the runtime cache yet, we * may have it remembered in a previous application instance. */ if (discoverInfo == null) { ConfigurationService configurationService = getConfigService(); String capsPropertyName = getCapsPropertyName(caps); String xml = configurationService.getString(capsPropertyName); if ((xml != null) && (xml.length() != 0)) { IQProvider discoverInfoProvider = (IQProvider) ProviderManager.getInstance() .getIQProvider("query", "http://jabber.org/protocol/disco#info"); if (discoverInfoProvider != null) { XmlPullParser parser = new MXParser(); try { parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(new StringReader(xml)); // Start the parser. parser.next(); } catch (XmlPullParserException xppex) { parser = null; } catch (IOException ioex) { parser = null; } if (parser != null) { try { discoverInfo = (DiscoverInfo) discoverInfoProvider.parseIQ(parser); } catch (Exception ex) { } if (discoverInfo != null) { if (caps.isValid(discoverInfo)) caps2discoverInfo.put(caps, discoverInfo); else { logger.error( "Invalid DiscoverInfo for " + caps.getNodeVer() + ": " + discoverInfo); /* * The discoverInfo doesn't seem valid * according to the caps which means that we * must have stored invalid information. * Delete the invalid information in order * to not try to validate it again. */ configurationService.removeProperty(capsPropertyName); } } } } } } return discoverInfo; } } /** * Removes from, to and packet-id from <tt>info</tt>. * * @param info the {@link DiscoverInfo} that we'd like to cleanup. */ private static void cleanupDiscoverInfo(DiscoverInfo info) { info.setFrom(null); info.setTo(null); info.setPacketID(null); } /** * Gets the features of a specific <tt>DiscoverInfo</tt> in the form of a read-only * <tt>Feature</tt> <tt>Iterator<tt/> by calling the internal method {@link * DiscoverInfo#getFeatures()}. * * @param discoverInfo the <tt>DiscoverInfo</tt> the features of which are to be retrieved * @return a read-only <tt>Feature</tt> <tt>Iterator</tt> which lists the features of the * specified <tt>discoverInfo</tt> */ @SuppressWarnings("unchecked") private static Iterator<DiscoverInfo.Feature> getDiscoverInfoFeatures(DiscoverInfo discoverInfo) { Method getFeaturesMethod; try { getFeaturesMethod = DiscoverInfo.class.getDeclaredMethod("getFeatures"); } catch (NoSuchMethodException nsmex) { throw new UndeclaredThrowableException(nsmex); } getFeaturesMethod.setAccessible(true); try { return (Iterator<DiscoverInfo.Feature>) getFeaturesMethod.invoke(discoverInfo); } catch (IllegalAccessException iaex) { throw new UndeclaredThrowableException(iaex); } catch (InvocationTargetException itex) { throw new UndeclaredThrowableException(itex); } } /** * Registers this Manager's listener with <tt>connection</tt>. * * @param connection the connection that we'd like this manager to register with. */ public void addPacketListener(XMPPConnection connection) { PacketFilter filter = new AndFilter( new PacketTypeFilter(Presence.class), new PacketExtensionFilter( CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE)); connection.addPacketListener(new CapsPacketListener(), filter); } /** * Adds <tt>listener</tt> to the list of {@link CapsVerListener}s that we notify when new features * occur and the version hash needs to be regenerated. The method would also notify * <tt>listener</tt> if our current caps version has been generated and is different than * <tt>null</tt>. * * @param listener the {@link CapsVerListener} we'd like to register. */ public void addCapsVerListener(CapsVerListener listener) { synchronized (capsVerListeners) { if (capsVerListeners.contains(listener)) return; capsVerListeners.add(listener); if (currentCapsVersion != null) listener.capsVerUpdated(currentCapsVersion); } } /** * Removes <tt>listener</tt> from the list of currently registered {@link CapsVerListener}s. * * @param listener the {@link CapsVerListener} we'd like to unregister. */ public void removeCapsVerListener(CapsVerListener listener) { synchronized (capsVerListeners) { capsVerListeners.remove(listener); } } /** * Notifies all currently registered {@link CapsVerListener}s that the version hash has changed. */ private void fireCapsVerChanged() { List<CapsVerListener> listenersCopy = null; synchronized (capsVerListeners) { listenersCopy = new ArrayList<CapsVerListener>(capsVerListeners); } for (CapsVerListener listener : listenersCopy) listener.capsVerUpdated(currentCapsVersion); } /** * Computes and returns the hash of the specified <tt>capsString</tt> using the specified * <tt>hashAlgorithm</tt>. * * @param hashAlgorithm the name of the algorithm to be used to generate the hash * @param capsString the capabilities string that we'd like to compute a hash for. * @return the hash of <tt>capsString</tt> computed by the specified <tt>hashAlgorithm</tt> or * <tt>null</tt> if generating the hash has failed */ private static String capsToHash(String hashAlgorithm, String capsString) { try { MessageDigest md = MessageDigest.getInstance(hashAlgorithm); byte[] digest = md.digest(capsString.getBytes()); return Base64.encodeBytes(digest); } catch (NoSuchAlgorithmException nsae) { logger.error("Unsupported XEP-0115: Entity Capabilities hash algorithm: " + hashAlgorithm); return null; } } /** * Converts the form field values in the <tt>ffValuesIter</tt> into a caps string. * * @param ffValuesIter the {@link Iterator} containing the form field values. * @param capsBldr a <tt>StringBuilder</tt> to which the caps string representing the form field * values is to be appended */ private static void formFieldValuesToCaps(Iterator<String> ffValuesIter, StringBuilder capsBldr) { SortedSet<String> fvs = new TreeSet<String>(); while (ffValuesIter.hasNext()) fvs.add(ffValuesIter.next()); for (String fv : fvs) capsBldr.append(fv).append('<'); } /** * Calculates the <tt>String</tt> for a specific <tt>DiscoverInfo</tt> which is to be hashed in * order to compute the ver string for that <tt>DiscoverInfo</tt>. * * @param discoverInfo the <tt>DiscoverInfo</tt> for which the <tt>String</tt> to be hashed in * order to compute its ver string is to be calculated * @return the <tt>String</tt> for <tt>discoverInfo</tt> which is to be hashed in order to compute * its ver string */ private static String calculateEntityCapsString(DiscoverInfo discoverInfo) { StringBuilder bldr = new StringBuilder(); // Add identities { Iterator<DiscoverInfo.Identity> identities = discoverInfo.getIdentities(); SortedSet<DiscoverInfo.Identity> is = new TreeSet<DiscoverInfo.Identity>( new Comparator<DiscoverInfo.Identity>() { public int compare(DiscoverInfo.Identity i1, DiscoverInfo.Identity i2) { int category = i1.getCategory().compareTo(i2.getCategory()); if (category != 0) return category; int type = i1.getType().compareTo(i2.getType()); if (type != 0) return type; /* * TODO Sort by xml:lang. * * Since sort by xml:lang is currently missing, * use the last supported sort criterion i.e. * type. */ return type; } }); if (identities != null) while (identities.hasNext()) is.add(identities.next()); for (DiscoverInfo.Identity i : is) { bldr.append(i.getCategory()) .append('/') .append(i.getType()) .append("//") .append(i.getName()) .append('<'); } } // Add features { Iterator<DiscoverInfo.Feature> features = getDiscoverInfoFeatures(discoverInfo); SortedSet<String> fs = new TreeSet<String>(); if (features != null) while (features.hasNext()) fs.add(features.next().getVar()); for (String f : fs) bldr.append(f).append('<'); } DataForm extendedInfo = (DataForm) discoverInfo.getExtension("x", "jabber:x:data"); if (extendedInfo != null) { synchronized (extendedInfo) { SortedSet<FormField> fs = new TreeSet<FormField>( new Comparator<FormField>() { public int compare(FormField f1, FormField f2) { return f1.getVariable().compareTo(f2.getVariable()); } }); FormField formType = null; for (Iterator<FormField> fieldsIter = extendedInfo.getFields(); fieldsIter.hasNext(); ) { FormField f = fieldsIter.next(); if (!f.getVariable().equals("FORM_TYPE")) fs.add(f); else formType = f; } // Add FORM_TYPE values if (formType != null) formFieldValuesToCaps(formType.getValues(), bldr); // Add the other values for (FormField f : fs) { bldr.append(f.getVariable()).append('<'); formFieldValuesToCaps(f.getValues(), bldr); } } } return bldr.toString(); } /** * Calculates the ver string for the specified <tt>discoverInfo</tt>, identity type, name * features, and extendedInfo. * * @param discoverInfo the {@link DiscoverInfo} we'd be creating a ver <tt>String</tt> for */ public void calculateEntityCapsVersion(DiscoverInfo discoverInfo) { setCurrentCapsVersion( discoverInfo, capsToHash(CapsPacketExtension.HASH_METHOD, calculateEntityCapsString(discoverInfo))); } /** * Set our own caps version. * * @param discoverInfo the {@link DiscoverInfo} that we'd like to map to the <tt>capsVersion</tt>. * @param capsVersion the new caps version */ public void setCurrentCapsVersion(DiscoverInfo discoverInfo, String capsVersion) { Caps caps = new Caps(getNode(), CapsPacketExtension.HASH_METHOD, capsVersion, null); /* * DiscoverInfo carries the node and the ver and we're now setting a new * ver so we should update the DiscoveryInfo. */ discoverInfo.setNode(caps.getNodeVer()); if (!caps.isValid(discoverInfo)) { throw new IllegalArgumentException( "The specified discoverInfo must be valid with respect" + " to the specified capsVersion"); } currentCapsVersion = capsVersion; addDiscoverInfoByCaps(caps, discoverInfo); fireCapsVerChanged(); } /** The {@link PacketListener} that will be registering incoming caps. */ private class CapsPacketListener implements PacketListener { /** * Handles incoming presence packets and maps jids to node#ver strings. * * @param packet the incoming presence <tt>Packet</tt> to be handled * @see PacketListener#processPacket(Packet) */ public void processPacket(Packet packet) { CapsPacketExtension ext = (CapsPacketExtension) packet.getExtension(CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE); /* * Before Version 1.4 of XEP-0115: Entity Capabilities, the 'ver' * attribute was generated differently and the 'hash' attribute was * absent. The 'ver' attribute in Version 1.3 represents the * specific version of the client and thus does not provide a way to * validate the DiscoverInfo sent by the client. If * EntityCapsManager receives no 'hash' attribute, it will assume * the legacy format and will not cache it because the DiscoverInfo * to be received from the client later on will not be trustworthy. */ String hash = ext.getHash(); /* Google Talk web does not set hash but we need it to be cached */ if (hash == null) hash = ""; if (hash != null) { // Check it the packet indicates that the user is online. We // will use this information to decide if we're going to send // the discover info request. boolean online = (packet instanceof Presence) && ((Presence) packet).isAvailable(); if (online) { addUserCapsNode( packet.getFrom(), ext.getNode(), hash, ext.getVersion(), ext.getExtensions(), online); } else { removeUserCapsNode(packet.getFrom()); } } } } /** * Implements an immutable value which stands for a specific node, a specific hash (algorithm) and * a specific ver. * * @author Lyubomir Marinov */ public static class Caps { /** The hash (algorithm) of this <tt>Caps</tt> value. */ public final String hash; /** The node of this <tt>Caps</tt> value. */ public final String node; /** The ext info of this <tt>Caps</tt> value. */ public String ext; /** * The String which is the concatenation of {@link #node} and the {@link #ver} separated by the * character '#'. Cached for the sake of efficiency. */ private final String nodeVer; /** The ver of this <tt>Caps</tt> value. */ public final String ver; /** * Initializes a new <tt>Caps</tt> instance which is to represent a specific node, a specific * hash (algorithm) and a specific ver. * * @param node the node to be represented by the new instance * @param hash the hash (algorithm) to be represented by the new instance * @param ver the ver to be represented by the new instance * @param ext the ext to be represented by the new instance */ public Caps(String node, String hash, String ver, String ext) { if (node == null) throw new NullPointerException("node"); if (hash == null) throw new NullPointerException("hash"); if (ver == null) throw new NullPointerException("ver"); this.node = node; this.hash = hash; this.ver = ver; this.ext = ext; this.nodeVer = this.node + '#' + this.ver; } /** * Gets a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of * this instance, the character '#' and the <tt>ver</tt> property of this instance. * * @return a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of * this instance, the character '#' and the <tt>ver</tt> property of this instance */ public final String getNodeVer() { return nodeVer; } /** * Determines whether a specific <tt>DiscoverInfo</tt> is valid according to this <tt>Caps</tt> * i.e. whether the <tt>discoverInfo</tt> has the node and the ver of this <tt>Caps</tt> and the * ver calculated from the <tt>discoverInfo</tt> using the hash (algorithm) of this * <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>. * * @param discoverInfo the <tt>DiscoverInfo</tt> to be validated by this <tt>Caps</tt> * @return <tt>true</tt> if the specified <tt>DiscoverInfo</tt> has the node and the ver of this * <tt>Caps</tt> and the ver calculated from the <tt>discoverInfo</tt> using the hash * (algorithm) of this <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>; otherwise, * <tt>false</tt> */ public boolean isValid(DiscoverInfo discoverInfo) { if (discoverInfo != null) { // The "node" attribute is not necessary in the query element. // For example, Swift does not send back the "node" attribute in // the Disco#info response. Thus, if the node of the IQ response // is null, then we set it to the request one. if (discoverInfo.getNode() == null) { discoverInfo.setNode(getNodeVer()); } if (getNodeVer().equals(discoverInfo.getNode()) && !hash.equals("") && ver.equals(capsToHash(hash, calculateEntityCapsString(discoverInfo)))) { return true; } } return false; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Caps caps = (Caps) o; if (!hash.equals(caps.hash)) return false; if (!node.equals(caps.node)) return false; if (!ver.equals(caps.ver)) return false; return true; } @Override public int hashCode() { int result = hash.hashCode(); result = 31 * result + node.hashCode(); result = 31 * result + ver.hashCode(); return result; } } }
/** * The <tt>BasicRTCPTerminationStrategy</tt> "gateways" PLIs, FIRs, NACKs, etc, in the sense that it * replaces the packet sender information in the PLIs, FIRs, NACKs, etc and it generates its own * SRs/RRs/REMBs based on information that it collects and from information found in FMJ. * * @author George Politis */ public class BasicRTCPTerminationStrategy extends MediaStreamRTCPTerminationStrategy { /** * The <tt>Logger</tt> used by the <tt>BasicRTCPTerminationStrategy</tt> class and its instances * to print debug information. */ private static final Logger logger = Logger.getLogger(BasicRTCPTerminationStrategy.class); /** The maximum number of RTCP report blocks that an RR or an SR can contain. */ private static final int MAX_RTCP_REPORT_BLOCKS = 31; /** The minimum number of RTCP report blocks that an RR or an SR can contain. */ private static final int MIN_RTCP_REPORT_BLOCKS = 0; /** * A reusable array that can be used to hold up to <tt>MAX_RTCP_REPORT_BLOCKS</tt> * <tt>RTCPReportBlock</tt>s. It is assumed that a single thread is accessing this field at a * given time. */ private final RTCPReportBlock[] MAX_RTCP_REPORT_BLOCKS_ARRAY = new RTCPReportBlock[MAX_RTCP_REPORT_BLOCKS]; /** A reusable array that holds 0 <tt>RTCPReportBlock</tt>s. */ private static final RTCPReportBlock[] MIN_RTCP_REPORTS_BLOCKS_ARRAY = new RTCPReportBlock[MIN_RTCP_REPORT_BLOCKS]; /** * The RTP stats map that holds RTP statistics about all the streams that this * <tt>BasicRTCPTerminationStrategy</tt> (as a <tt>TransformEngine</tt>) has observed. */ private final RTPStatsMap rtpStatsMap = new RTPStatsMap(); /** * The RTCP stats map that holds RTCP statistics about all the streams that this * <tt>BasicRTCPTerminationStrategy</tt> (as a <tt>TransformEngine</tt>) has observed. */ private final RemoteClockEstimator remoteClockEstimator = new RemoteClockEstimator(); /** * The <tt>CNameRegistry</tt> holds the CNAMEs that this RTCP termination, seen as a * TransformEngine, has seen. */ private final CNAMERegistry cnameRegistry = new CNAMERegistry(); /** The parser that parses <tt>RawPacket</tt>s to <tt>RTCPCompoundPacket</tt>s. */ private final RTCPPacketParserEx parser = new RTCPPacketParserEx(); /** The generator that generates <tt>RawPacket</tt>s from <tt>RTCPCompoundPacket</tt>s. */ private final RTCPGenerator generator = new RTCPGenerator(); /** * The RTCP feedback gateway responsible for dropping all the stuff that we support in this RTCP * termination strategy. */ private final FeedbackGateway feedbackGateway = new FeedbackGateway(); /** The garbage collector that cleans-up the state of this RTCP termination strategy. */ private final GarbageCollector garbageCollector = new GarbageCollector(); /** The RTP <tt>PacketTransformer</tt> of this <tt>BasicRTCPTerminationStrategy</tt>. */ private final PacketTransformer rtpTransformer = new SinglePacketTransformer() { /** {@inheritDoc} */ @Override public RawPacket transform(RawPacket pkt) { // Update our RTP stats map (packets/octet sent). rtpStatsMap.apply(pkt); return pkt; } /** {@inheritDoc} */ @Override public RawPacket reverseTransform(RawPacket pkt) { // Let everything pass through. return pkt; } }; /** The RTCP <tt>PacketTransformer</tt> of this <tt>BasicRTCPTerminationStrategy</tt>. */ private final PacketTransformer rtcpTransformer = new SinglePacketTransformer() { /** {@inheritDoc} */ @Override public RawPacket transform(RawPacket pkt) { if (pkt == null) { return pkt; } RTCPCompoundPacket inPacket; try { inPacket = (RTCPCompoundPacket) parser.parse(pkt.getBuffer(), pkt.getOffset(), pkt.getLength()); } catch (BadFormatException e) { logger.warn("Failed to terminate an RTCP packet. " + "Dropping packet."); return null; } // Update our RTCP stats map (timestamps). This operation is // read-only. remoteClockEstimator.apply(inPacket); cnameRegistry.update(inPacket); // Remove SRs and RRs from the RTCP packet. pkt = feedbackGateway.gateway(inPacket); return pkt; } /** {@inheritDoc} */ @Override public RawPacket reverseTransform(RawPacket pkt) { // Let everything pass through. return pkt; } }; /** A counter that counts the number of times we've sent "full-blown" SDES. */ private int sdesCounter = 0; /** {@inheritDoc} */ @Override public PacketTransformer getRTPTransformer() { return rtpTransformer; } /** {@inheritDoc} */ @Override public PacketTransformer getRTCPTransformer() { return rtcpTransformer; } /** {@inheritDoc} */ @Override public RawPacket report() { garbageCollector.cleanup(); // TODO Compound RTCP packets should not exceed the MTU of the network // path. // // An individual RTP participant should send only one compound RTCP // packet per report interval in order for the RTCP bandwidth per // participant to be estimated correctly, except when the compound // RTCP packet is split for partial encryption. // // If there are too many sources to fit all the necessary RR packets // into one compound RTCP packet without exceeding the maximum // transmission unit (MTU) of the network path, then only the subset // that will fit into one MTU should be included in each interval. The // subsets should be selected round-robin across multiple intervals so // that all sources are reported. // // It is impossible to know in advance what the MTU of path will be. // There are various algorithms for experimenting to find out, but many // devices do not properly implement (or deliberately ignore) the // necessary standards so it all comes down to trial and error. For that // reason, we can just guess 1200 or 1500 bytes per message. long time = System.currentTimeMillis(); Collection<RTCPPacket> packets = new ArrayList<RTCPPacket>(); // First, we build the RRs. Collection<RTCPRRPacket> rrPackets = makeRTCPRRPackets(time); if (rrPackets != null && rrPackets.size() != 0) { packets.addAll(rrPackets); } // Next, we build the SRs. Collection<RTCPSRPacket> srPackets = makeRTCPSRPackets(time); if (srPackets != null && srPackets.size() != 0) { packets.addAll(srPackets); } // Bail out if we have nothing to report. if (packets.size() == 0) { return null; } // Next, we build the REMB. RTCPREMBPacket rembPacket = makeRTCPREMBPacket(); if (rembPacket != null) { packets.add(rembPacket); } // Finally, we add an SDES packet. RTCPSDESPacket sdesPacket = makeSDESPacket(); if (sdesPacket != null) { packets.add(sdesPacket); } // Prepare the <tt>RTCPCompoundPacket</tt> to return. RTCPPacket rtcpPackets[] = packets.toArray(new RTCPPacket[packets.size()]); RTCPCompoundPacket cp = new RTCPCompoundPacket(rtcpPackets); // Build the <tt>RTCPCompoundPacket</tt> and return the // <tt>RawPacket</tt> to inject to the <tt>MediaStream</tt>. return generator.apply(cp); } /** * (attempts) to get the local SSRC that will be used in the media sender SSRC field of the RTCP * reports. TAG(cat4-local-ssrc-hurricane) * * @return */ private long getLocalSSRC() { return getStream().getStreamRTPManager().getLocalSSRC(); } /** * Makes <tt>RTCPRRPacket</tt>s using information in FMJ. * * @param time * @return A <tt>Collection</tt> of <tt>RTCPRRPacket</tt>s to inject to the <tt>MediaStream</tt>. */ private Collection<RTCPRRPacket> makeRTCPRRPackets(long time) { RTCPReportBlock[] reportBlocks = makeRTCPReportBlocks(time); if (reportBlocks == null || reportBlocks.length == 0) { return null; } Collection<RTCPRRPacket> rrPackets = new ArrayList<RTCPRRPacket>(); // We use the stream's local source ID (SSRC) as the SSRC of packet // sender. long streamSSRC = getLocalSSRC(); // Since a maximum of 31 reception report blocks will fit in an SR // or RR packet, additional RR packets SHOULD be stacked after the // initial SR or RR packet as needed to contain the reception // reports for all sources heard during the interval since the last // report. if (reportBlocks.length > MAX_RTCP_REPORT_BLOCKS) { for (int offset = 0; offset < reportBlocks.length; offset += MAX_RTCP_REPORT_BLOCKS) { RTCPReportBlock[] blocks = (reportBlocks.length - offset < MAX_RTCP_REPORT_BLOCKS) ? new RTCPReportBlock[reportBlocks.length - offset] : MAX_RTCP_REPORT_BLOCKS_ARRAY; System.arraycopy(reportBlocks, offset, blocks, 0, blocks.length); RTCPRRPacket rr = new RTCPRRPacket((int) streamSSRC, blocks); rrPackets.add(rr); } } else { RTCPRRPacket rr = new RTCPRRPacket((int) streamSSRC, reportBlocks); rrPackets.add(rr); } return rrPackets; } /** * Iterate through all the <tt>ReceiveStream</tt>s that this <tt>MediaStream</tt> has and make * <tt>RTCPReportBlock</tt>s for all of them. * * @param time * @return */ private RTCPReportBlock[] makeRTCPReportBlocks(long time) { MediaStream stream = getStream(); // State validation. if (stream == null) { logger.warn("stream is null."); return MIN_RTCP_REPORTS_BLOCKS_ARRAY; } StreamRTPManager streamRTPManager = stream.getStreamRTPManager(); if (streamRTPManager == null) { logger.warn("streamRTPManager is null."); return MIN_RTCP_REPORTS_BLOCKS_ARRAY; } Collection<ReceiveStream> receiveStreams = streamRTPManager.getReceiveStreams(); if (receiveStreams == null || receiveStreams.size() == 0) { logger.info("There are no receive streams to build report " + "blocks for."); return MIN_RTCP_REPORTS_BLOCKS_ARRAY; } SSRCCache cache = streamRTPManager.getSSRCCache(); if (cache == null) { logger.info("cache is null."); return MIN_RTCP_REPORTS_BLOCKS_ARRAY; } // Create the return object. Collection<RTCPReportBlock> rtcpReportBlocks = new ArrayList<RTCPReportBlock>(); // Populate the return object. for (ReceiveStream receiveStream : receiveStreams) { // Dig into the guts of FMJ and get the stats for the current // receiveStream. SSRCInfo info = cache.cache.get((int) receiveStream.getSSRC()); if (!info.ours && info.sender) { RTCPReportBlock rtcpReportBlock = info.makeReceiverReport(time); rtcpReportBlocks.add(rtcpReportBlock); } } return rtcpReportBlocks.toArray(new RTCPReportBlock[rtcpReportBlocks.size()]); } /** * Makes an <tt>RTCPREMBPacket</tt> that provides receiver feedback to the endpoint from which we * receive. * * @return an <tt>RTCPREMBPacket</tt> that provides receiver feedback to the endpoint from which * we receive. */ private RTCPREMBPacket makeRTCPREMBPacket() { // TODO we should only make REMBs if REMB support has been advertised. // Destination RemoteBitrateEstimator remoteBitrateEstimator = ((VideoMediaStream) getStream()).getRemoteBitrateEstimator(); Collection<Integer> ssrcs = remoteBitrateEstimator.getSsrcs(); // TODO(gp) intersect with SSRCs from signaled simulcast layers // NOTE(gp) The Google Congestion Control algorithm (sender side) // doesn't seem to care about the SSRCs in the dest field. long[] dest = new long[ssrcs.size()]; int i = 0; for (Integer ssrc : ssrcs) dest[i++] = ssrc & 0xFFFFFFFFL; // Exp & mantissa long bitrate = remoteBitrateEstimator.getLatestEstimate(); if (bitrate == -1) return null; if (logger.isDebugEnabled()) logger.debug("Estimated bitrate: " + bitrate); // Create and return the packet. // We use the stream's local source ID (SSRC) as the SSRC of packet // sender. long streamSSRC = getLocalSSRC(); return new RTCPREMBPacket(streamSSRC, /* mediaSSRC */ 0L, bitrate, dest); } /** * Makes <tt>RTCPSRPacket</tt>s for all the RTP streams that we're sending. * * @return a <tt>List</tt> of <tt>RTCPSRPacket</tt> for all the RTP streams that we're sending. */ private Collection<RTCPSRPacket> makeRTCPSRPackets(long time) { Collection<RTCPSRPacket> srPackets = new ArrayList<RTCPSRPacket>(); for (RTPStatsEntry rtpStatsEntry : rtpStatsMap.values()) { int ssrc = rtpStatsEntry.getSsrc(); RemoteClock estimate = remoteClockEstimator.estimate(ssrc, time); if (estimate == null) { // We're not going to go far without an estimate.. continue; } RTCPSRPacket srPacket = new RTCPSRPacket(ssrc, MIN_RTCP_REPORTS_BLOCKS_ARRAY); // Set the NTP timestamp for this SR. long estimatedRemoteTime = estimate.getRemoteTime(); long secs = estimatedRemoteTime / 1000L; double fraction = (estimatedRemoteTime - secs * 1000L) / 1000D; srPacket.ntptimestamplsw = (int) (fraction * 4294967296D); srPacket.ntptimestampmsw = secs; // Set the RTP timestamp. srPacket.rtptimestamp = estimate.getRtpTimestamp(); // Fill-in packet and octet send count. srPacket.packetcount = rtpStatsEntry.getPacketsSent(); srPacket.octetcount = rtpStatsEntry.getBytesSent(); srPackets.add(srPacket); } return srPackets; } /** * Makes <tt>RTCPSDES</tt> packets for all the RTP streams that we're sending. * * @return a <tt>List</tt> of <tt>RTCPSDES</tt> packets for all the RTP streams that we're * sending. */ private RTCPSDESPacket makeSDESPacket() { Collection<RTCPSDES> sdesChunks = new ArrayList<RTCPSDES>(); // Create an SDES for our own SSRC. RTCPSDES ownSDES = new RTCPSDES(); SSRCInfo ourinfo = getStream().getStreamRTPManager().getSSRCCache().ourssrc; ownSDES.ssrc = (int) getLocalSSRC(); Collection<RTCPSDESItem> ownItems = new ArrayList<RTCPSDESItem>(); ownItems.add(new RTCPSDESItem(RTCPSDESItem.CNAME, ourinfo.sourceInfo.getCNAME())); // Throttle the source description bandwidth. See RFC3550#6.3.9 // Allocation of Source Description Bandwidth. if (sdesCounter % 3 == 0) { if (ourinfo.name != null && ourinfo.name.getDescription() != null) ownItems.add(new RTCPSDESItem(RTCPSDESItem.NAME, ourinfo.name.getDescription())); if (ourinfo.email != null && ourinfo.email.getDescription() != null) ownItems.add(new RTCPSDESItem(RTCPSDESItem.EMAIL, ourinfo.email.getDescription())); if (ourinfo.phone != null && ourinfo.phone.getDescription() != null) ownItems.add(new RTCPSDESItem(RTCPSDESItem.PHONE, ourinfo.phone.getDescription())); if (ourinfo.loc != null && ourinfo.loc.getDescription() != null) ownItems.add(new RTCPSDESItem(RTCPSDESItem.LOC, ourinfo.loc.getDescription())); if (ourinfo.tool != null && ourinfo.tool.getDescription() != null) ownItems.add(new RTCPSDESItem(RTCPSDESItem.TOOL, ourinfo.tool.getDescription())); if (ourinfo.note != null && ourinfo.note.getDescription() != null) ownItems.add(new RTCPSDESItem(RTCPSDESItem.NOTE, ourinfo.note.getDescription())); } sdesCounter++; ownSDES.items = ownItems.toArray(new RTCPSDESItem[ownItems.size()]); sdesChunks.add(ownSDES); for (Map.Entry<Integer, byte[]> entry : cnameRegistry.entrySet()) { RTCPSDES sdes = new RTCPSDES(); sdes.ssrc = entry.getKey(); sdes.items = new RTCPSDESItem[] {new RTCPSDESItem(RTCPSDESItem.CNAME, entry.getValue())}; } RTCPSDES[] sps = sdesChunks.toArray(new RTCPSDES[sdesChunks.size()]); RTCPSDESPacket sp = new RTCPSDESPacket(sps); return sp; } /** * The garbage collector runs at each reporting interval and cleans up the data structures of this * RTCP termination strategy based on the SSRCs that the owner <tt>MediaStream</tt> is still * sending. */ class GarbageCollector { public void cleanup() { // TODO We need to fix TAG(cat4-local-ssrc-hurricane) and // TAG(cat4-remote-ssrc-hurricane) first. The idea is to remove // from our data structures everything that is not listed in as // a remote SSRC. } } /** * Removes receiver and sender feedback from RTCP packets. Typically this means dropping SRs, RR * report blocks and REMBs. It needs to pass through PLIs, FIRs, NACKs, etc. */ class FeedbackGateway { /** * Removes receiver and sender feedback from RTCP packets. * * @param inPacket the <tt>RTCPCompoundPacket</tt> to filter. * @return the filtered <tt>RawPacket</tt>. */ public RawPacket gateway(RTCPCompoundPacket inPacket) { if (inPacket == null || inPacket.packets == null || inPacket.packets.length == 0) { logger.info("Ignoring empty RTCP packet."); return null; } ArrayList<RTCPPacket> outPackets = new ArrayList<RTCPPacket>(inPacket.packets.length); for (RTCPPacket p : inPacket.packets) { switch (p.type) { case RTCPPacket.RR: case RTCPPacket.SR: case RTCPPacket.SDES: // We generate our own RR/SR/SDES packets. We only want // to forward NACKs/PLIs/etc. break; case RTCPFBPacket.PSFB: RTCPFBPacket psfb = (RTCPFBPacket) p; switch (psfb.fmt) { case RTCPREMBPacket.FMT: // We generate its own REMB packets. break; default: // We let through everything else, like NACK // packets. outPackets.add(psfb); break; } break; default: // We let through everything else, like BYE and APP // packets. outPackets.add(p); break; } } if (outPackets.size() == 0) { return null; } // We have feedback messages to send. Pack them in a compound // RR and send them. TODO Use RFC5506 Reduced-Size RTCP, if the // receiver supports it. Collection<RTCPRRPacket> rrPackets = makeRTCPRRPackets(System.currentTimeMillis()); if (rrPackets != null && rrPackets.size() != 0) { outPackets.addAll(0, rrPackets); } else { logger.warn("We might be sending invalid RTCPs."); } RTCPPacket[] pkts = outPackets.toArray(new RTCPPacket[outPackets.size()]); RTCPCompoundPacket outPacket = new RTCPCompoundPacket(pkts); return generator.apply(outPacket); } } /** Holds the NTP timestamp and the associated RTP timestamp for a given RTP stream. */ class RemoteClock { /** * Ctor. * * @param remoteTime * @param rtpTimestamp */ public RemoteClock(long remoteTime, int rtpTimestamp) { this.remoteTime = remoteTime; this.rtpTimestamp = rtpTimestamp; } /** * The last NTP timestamp that we received for {@link this.ssrc} expressed in millis. Should be * treated a signed long. */ private final long remoteTime; /** * The RTP timestamp associated to {@link this.ntpTimestamp}. The RTP timestamp is an unsigned * int. */ private final int rtpTimestamp; /** @return */ public int getRtpTimestamp() { return rtpTimestamp; } /** @return */ public long getRemoteTime() { return remoteTime; } } /** */ class ReceivedRemoteClock { /** The SSRC. */ private final int ssrc; /** * The <tt>RemoteClock</tt> which was received at {@link this.receivedTime} for this RTP stream. */ private final RemoteClock remoteClock; /** * The local time in millis when we received the RTCP report with the RTP/NTP timestamps. It's a * signed long. */ private final long receivedTime; /** * The clock rate for {@link.ssrc}. We need to have received at least two SRs in order to be * able to calculate this. Unsigned short. */ private final int frequencyHz; /** * Ctor. * * @param ssrc * @param remoteTime * @param rtpTimestamp * @param frequencyHz */ ReceivedRemoteClock(int ssrc, long remoteTime, int rtpTimestamp, int frequencyHz) { this.ssrc = ssrc; this.remoteClock = new RemoteClock(remoteTime, rtpTimestamp); this.frequencyHz = frequencyHz; this.receivedTime = System.currentTimeMillis(); } /** @return */ public RemoteClock getRemoteClock() { return remoteClock; } /** @return */ public long getReceivedTime() { return receivedTime; } /** @return */ public int getSsrc() { return ssrc; } /** @return */ public int getFrequencyHz() { return frequencyHz; } } /** The <tt>RTPStatsEntry</tt> class contains information about an outgoing SSRC. */ class RTPStatsEntry { /** The SSRC of the stream that this instance tracks. */ private final int ssrc; /** * The total number of _payload_ octets (i.e., not including header or padding) transmitted in * RTP data packets by the sender since starting transmission up until the time this SR packet * was generated. This should be treated as an unsigned int. */ private final int bytesSent; /** * The total number of RTP data packets transmitted by the sender (including re-transmissions) * since starting transmission up until the time this SR packet was generated. Re-transmissions * using an RTX stream are tracked in the RTX SSRC. This should be treated as an unsigned int. */ private final int packetsSent; /** @return */ public int getSsrc() { return ssrc; } /** @return */ public int getBytesSent() { return bytesSent; } /** @return */ public int getPacketsSent() { return packetsSent; } /** * Ctor. * * @param ssrc * @param bytesSent */ RTPStatsEntry(int ssrc, int bytesSent, int packetsSent) { this.ssrc = ssrc; this.bytesSent = bytesSent; this.packetsSent = packetsSent; } } /** * The <tt>RtpStatsMap</tt> gathers stats from RTP packets that the <tt>RTCPReportBuilder</tt> * uses to build its reports. */ class RTPStatsMap extends ConcurrentHashMap<Integer, RTPStatsEntry> { /** * Updates this <tt>RTPStatsMap</tt> with information it gets from the <tt>RawPacket</tt>. * * @param pkt the <tt>RawPacket</tt> that is being transmitted. */ public void apply(RawPacket pkt) { int ssrc = pkt.getSSRC(); if (this.containsKey(ssrc)) { RTPStatsEntry oldRtpStatsEntry = this.get(ssrc); // Replace whatever was in there before. A feature of the two's // complement encoding (which is used by Java integers) is that // the bitwise results for add, subtract, and multiply are the // same if both inputs are interpreted as signed values or both // inputs are interpreted as unsigned values. (Other encodings // like one's complement and signed magnitude don't have this // properly.) this.put( ssrc, new RTPStatsEntry( ssrc, oldRtpStatsEntry.getBytesSent() + pkt.getLength() - pkt.getHeaderLength() - pkt.getPaddingSize(), oldRtpStatsEntry.getPacketsSent() + 1)); } else { // Add a new <tt>RTPStatsEntry</tt> in this map. this.put( ssrc, new RTPStatsEntry( ssrc, pkt.getLength() - pkt.getHeaderLength() - pkt.getPaddingSize(), 1)); } } } /** A class that can be used to estimate the remote time at a given local time. */ class RemoteClockEstimator { /** base: 7-Feb-2036 @ 06:28:16 UTC */ private static final long msb0baseTime = 2085978496000L; /** base: 1-Jan-1900 @ 01:00:00 UTC */ private static final long msb1baseTime = -2208988800000L; /** A map holding the received remote clocks. */ private Map<Integer, ReceivedRemoteClock> receivedClocks = new ConcurrentHashMap<Integer, ReceivedRemoteClock>(); /** * Inspect an <tt>RTCPCompoundPacket</tt> and build-up the state for future estimations. * * @param pkt */ public void apply(RTCPCompoundPacket pkt) { if (pkt == null || pkt.packets == null || pkt.packets.length == 0) { return; } for (RTCPPacket rtcpPacket : pkt.packets) { switch (rtcpPacket.type) { case RTCPPacket.SR: RTCPSRPacket srPacket = (RTCPSRPacket) rtcpPacket; // The media sender SSRC. int ssrc = srPacket.ssrc; // Convert 64-bit NTP timestamp to Java standard time. // Note that java time (milliseconds) by definition has // less precision then NTP time (picoseconds) so // converting NTP timestamp to java time and back to NTP // timestamp loses precision. For example, Tue, Dec 17 // 2002 09:07:24.810 EST is represented by a single // Java-based time value of f22cd1fc8a, but its NTP // equivalent are all values ranging from // c1a9ae1c.cf5c28f5 to c1a9ae1c.cf9db22c. // Use round-off on fractional part to preserve going to // lower precision long fraction = Math.round(1000D * srPacket.ntptimestamplsw / 0x100000000L); /* * If the most significant bit (MSB) on the seconds * field is set we use a different time base. The * following text is a quote from RFC-2030 (SNTP v4): * * If bit 0 is set, the UTC time is in the range * 1968-2036 and UTC time is reckoned from 0h 0m 0s UTC * on 1 January 1900. If bit 0 is not set, the time is * in the range 2036-2104 and UTC time is reckoned from * 6h 28m 16s UTC on 7 February 2036. */ long msb = srPacket.ntptimestampmsw & 0x80000000L; long remoteTime = (msb == 0) // use base: 7-Feb-2036 @ 06:28:16 UTC ? msb0baseTime + (srPacket.ntptimestampmsw * 1000) + fraction // use base: 1-Jan-1900 @ 01:00:00 UTC : msb1baseTime + (srPacket.ntptimestampmsw * 1000) + fraction; // Estimate the clock rate of the sender. int frequencyHz = -1; if (receivedClocks.containsKey(ssrc)) { // Calculate the clock rate. ReceivedRemoteClock oldStats = receivedClocks.get(ssrc); RemoteClock oldRemoteClock = oldStats.getRemoteClock(); frequencyHz = Math.round( (float) (((int) srPacket.rtptimestamp - oldRemoteClock.getRtpTimestamp()) & 0xffffffffl) / (remoteTime - oldRemoteClock.getRemoteTime())); } // Replace whatever was in there before. receivedClocks.put( ssrc, new ReceivedRemoteClock( ssrc, remoteTime, (int) srPacket.rtptimestamp, frequencyHz)); break; case RTCPPacket.SDES: break; } } } /** * Estimate the <tt>RemoteClock</tt> of a given RTP stream (identified by its SSRC) at a given * time. * * @param ssrc the SSRC of the RTP stream whose <tt>RemoteClock</tt> we want to estimate. * @param time the local time that will be mapped to a remote time. * @return An estimation of the <tt>RemoteClock</tt> at time "time". */ public RemoteClock estimate(int ssrc, long time) { ReceivedRemoteClock receivedRemoteClock = receivedClocks.get(ssrc); if (receivedRemoteClock == null || receivedRemoteClock.getFrequencyHz() == -1) { // We can't continue if we don't have NTP and RTP timestamps // and/or the original sender frequency, so move to the next // one. return null; } long delayMillis = time - receivedRemoteClock.getReceivedTime(); // Estimate the remote wall clock. long remoteTime = receivedRemoteClock.getRemoteClock().getRemoteTime(); long estimatedRemoteTime = remoteTime + delayMillis; // Drift the RTP timestamp. int rtpTimestamp = receivedRemoteClock.getRemoteClock().getRtpTimestamp() + ((int) delayMillis) * (receivedRemoteClock.getFrequencyHz() / 1000); return new RemoteClock(estimatedRemoteTime, rtpTimestamp); } } /** Keeps track of the CNAMEs of the RTP streams that we've seen. */ class CNAMERegistry extends ConcurrentHashMap<Integer, byte[]> { /** @param inPacket */ public void update(RTCPCompoundPacket inPacket) { // Update CNAMEs. if (inPacket == null || inPacket.packets == null || inPacket.packets.length == 0) { return; } for (RTCPPacket p : inPacket.packets) { switch (p.type) { case RTCPPacket.SDES: RTCPSDESPacket sdesPacket = (RTCPSDESPacket) p; if (sdesPacket.sdes == null || sdesPacket.sdes.length == 0) { continue; } for (RTCPSDES chunk : sdesPacket.sdes) { if (chunk.items == null || chunk.items.length == 0) { continue; } for (RTCPSDESItem sdesItm : chunk.items) { if (sdesItm.type != RTCPSDESItem.CNAME) { continue; } this.put(chunk.ssrc, sdesItm.data); } } break; } } } } }
/** * @author Bing SU ([email protected]) * @author Lyubomir Marinov * @author Boris Grozev */ public abstract class RTPConnectorInputStream implements PushSourceStream, Runnable { /** * The value of the property <tt>controls</tt> of <tt>RTPConnectorInputStream</tt> when there are * no controls. Explicitly defined in order to reduce unnecessary allocations. */ private static final Object[] EMPTY_CONTROLS = new Object[0]; /** * The length in bytes of the buffers of <tt>RTPConnectorInputStream</tt> receiving packets from * the network. */ public static final int PACKET_RECEIVE_BUFFER_LENGTH = 4 * 1024; /** * The <tt>Logger</tt> used by the <tt>RTPConnectorInputStream</tt> class and its instances to * print debug information. */ private static final Logger logger = Logger.getLogger(RTPConnectorInputStream.class); /** Packet receive buffer */ private final byte[] buffer = new byte[PACKET_RECEIVE_BUFFER_LENGTH]; /** Whether this stream is closed. Used to control the termination of worker thread. */ protected boolean closed; public Participant videoRecorder; /** * The <tt>DatagramPacketFilter</tt>s which allow dropping <tt>DatagramPacket</tt>s before they * are converted into <tt>RawPacket</tt>s. */ private DatagramPacketFilter[] datagramPacketFilters; /** Caught an IO exception during read from socket */ protected boolean ioError = false; /** * The packet data to be read out of this instance through its {@link #read(byte[], int, int)} * method. */ private RawPacket pkt; /** The <tt>Object</tt> which synchronizes the access to {@link #pkt}. */ private final Object pktSyncRoot = new Object(); /** The adapter of this <tt>PushSourceStream</tt> to the <tt>PushBufferStream</tt> interface. */ private final PushBufferStream pushBufferStream; /** * The pool of <tt>RawPacket[]</tt> instances to reduce their allocations and garbage collection. * Contains arrays full of <tt>null</tt>. */ private final Queue<RawPacket[]> rawPacketArrayPool = new LinkedBlockingQueue<RawPacket[]>(); /** * The pool of <tt>RawPacket</tt> instances to reduce their allocations and garbage collection. */ private final Queue<RawPacket> rawPacketPool = new LinkedBlockingQueue<RawPacket>(); /** The Thread receiving packets. */ protected Thread receiverThread = null; /** SourceTransferHandler object which is used to read packets. */ private SourceTransferHandler transferHandler; /** * Whether this <tt>RTPConnectorInputStream</tt> is enabled or disabled. While disabled, the * stream does not accept any packets. */ private boolean enabled = true; /** * Initializes a new <tt>RTPConnectorInputStream</tt> which is to receive packet data from a * specific UDP socket. */ public RTPConnectorInputStream() { // PacketLoggingService addDatagramPacketFilter( new DatagramPacketFilter() { /** * Used for debugging. As we don't log every packet, we must count them and decide which * to log. */ private long numberOfPackets = 0; public boolean accept(DatagramPacket p) { numberOfPackets++; if (RTPConnectorOutputStream.logPacket(numberOfPackets)) { PacketLoggingService packetLogging = LibJitsi.getPacketLoggingService(); if ((packetLogging != null) && packetLogging.isLoggingEnabled(PacketLoggingService.ProtocolName.RTP)) doLogPacket(p); } return true; } }); /* * Adapt this PushSourceStream to the PushBufferStream interface in * order to make it possible to read the Buffer flags of RawPacket. */ pushBufferStream = new PushBufferStreamAdapter(this, null) { @Override protected int doRead(Buffer buffer, byte[] data, int offset, int length) throws IOException { return RTPConnectorInputStream.this.read(buffer, data, offset, length); } }; } /** Close this stream, stops the worker thread. */ public synchronized void close() {} /** * Creates a new <tt>RawPacket</tt> from a specific <tt>DatagramPacket</tt> in order to have this * instance receive its packet data through its {@link #read(byte[], int, int)} method. Returns an * array of <tt>RawPacket</tt> with the created packet as its first element (and <tt>null</tt> for * the other elements). * * <p>Allows extenders to intercept the packet data and possibly filter and/or modify it. * * @param datagramPacket the <tt>DatagramPacket</tt> containing the packet data * @return an array of <tt>RawPacket</tt> containing the <tt>RawPacket</tt> which contains the * packet data of the specified <tt>DatagramPacket</tt> as its first element. */ protected RawPacket[] createRawPacket(DatagramPacket datagramPacket) { RawPacket[] pkts = rawPacketArrayPool.poll(); if (pkts == null) pkts = new RawPacket[1]; RawPacket pkt = rawPacketPool.poll(); if (pkt == null) pkt = new RawPacket(); pkt.setBuffer(datagramPacket.getData()); pkt.setFlags(0); pkt.setLength(datagramPacket.getLength()); pkt.setOffset(datagramPacket.getOffset()); pkts[0] = pkt; return pkts; } /** * Provides a dummy implementation to {@link RTPConnectorInputStream#endOfStream()} that always * returns <tt>false</tt>. * * @return <tt>false</tt>, no matter what. */ public boolean endOfStream() { return false; } /** * Provides a dummy implementation to {@link RTPConnectorInputStream#getContentDescriptor()} that * always returns <tt>null</tt>. * * @return <tt>null</tt>, no matter what. */ public ContentDescriptor getContentDescriptor() { return null; } /** * Provides a dummy implementation to {@link RTPConnectorInputStream#getContentLength()} that * always returns <tt>LENGTH_UNKNOWN</tt>. * * @return <tt>LENGTH_UNKNOWN</tt>, no matter what. */ public long getContentLength() { return LENGTH_UNKNOWN; } /** * Provides a dummy implementation of {@link RTPConnectorInputStream#getControl(String)} that * always returns <tt>null</tt>. * * @param controlType ignored. * @return <tt>null</tt>, no matter what. */ public Object getControl(String controlType) { if (PushBufferStream.class.getName().equals(controlType)) return pushBufferStream; else return null; } /** * Provides a dummy implementation of {@link RTPConnectorInputStream#getControls()} that always * returns <tt>EMPTY_CONTROLS</tt>. * * @return <tt>EMPTY_CONTROLS</tt>, no matter what. */ public Object[] getControls() { return EMPTY_CONTROLS; } /** * Provides a dummy implementation to {@link RTPConnectorInputStream#getMinimumTransferSize()} * that always returns <tt>2 * 1024</tt>. * * @return <tt>2 * 1024</tt>, no matter what. */ public int getMinimumTransferSize() { return 2 * 1024; // twice the MTU size, just to be safe. } /** * Pools the specified <tt>RawPacket</tt> in order to avoid future allocations and to reduce the * effects of garbage collection. * * @param pkt the <tt>RawPacket</tt> to be offered to {@link #rawPacketPool} */ private void poolRawPacket(RawPacket pkt) { pkt.setBuffer(null); pkt.setFlags(0); pkt.setLength(0); pkt.setOffset(0); rawPacketPool.offer(pkt); } /** * Copies the content of the most recently received packet into <tt>buffer</tt>. * * @param buffer the <tt>byte[]</tt> that we'd like to copy the content of the packet to. * @param offset the position where we are supposed to start writing in <tt>buffer</tt>. * @param length the number of <tt>byte</tt>s available for writing in <tt>buffer</tt>. * @return the number of bytes read * @throws IOException if <tt>length</tt> is less than the size of the packet. */ public int read(byte[] buffer, int offset, int length) throws IOException { return read(null, buffer, offset, length); } /** * Copies the content of the most recently received packet into <tt>data</tt>. * * @param buffer an optional <tt>Buffer</tt> instance associated with the specified <tt>data</tt>, * <tt>offset</tt> and <tt>length</tt> and provided to the method in case the implementation * would like to provide additional <tt>Buffer</tt> properties such as <tt>flags</tt> * @param data the <tt>byte[]</tt> that we'd like to copy the content of the packet to. * @param offset the position where we are supposed to start writing in <tt>data</tt>. * @param length the number of <tt>byte</tt>s available for writing in <tt>data</tt>. * @return the number of bytes read * @throws IOException if <tt>length</tt> is less than the size of the packet. */ protected int read(Buffer buffer, byte[] data, int offset, int length) throws IOException { if (data == null) throw new NullPointerException("data"); if (ioError) return -1; RawPacket pkt; synchronized (pktSyncRoot) { pkt = this.pkt; this.pkt = null; } int pktLength; if (pkt == null) { pktLength = 0; } else { // By default, pkt will be returned to the pool after it was read. boolean poolPkt = true; try { pktLength = pkt.getLength(); if (length < pktLength) { /* * If pkt is still the latest RawPacket made available to * reading, reinstate it for the next invocation of read; * otherwise, return it to the pool. */ poolPkt = false; throw new IOException("Input buffer not big enough for " + pktLength); } else { byte[] pktBuffer = pkt.getBuffer(); if (pktBuffer == null) { throw new NullPointerException( "pkt.buffer null, pkt.length " + pktLength + ", pkt.offset " + pkt.getOffset()); } else { System.arraycopy(pkt.getBuffer(), pkt.getOffset(), data, offset, pktLength); if (buffer != null) buffer.setFlags(pkt.getFlags()); } } } finally { if (!poolPkt) { synchronized (pktSyncRoot) { if (this.pkt == null) this.pkt = pkt; else poolPkt = true; } } if (poolPkt) { // Return pkt to the pool because it was successfully read. poolRawPacket(pkt); } } } return pktLength; } /** * Log the packet. * * @param packet packet to log */ protected abstract void doLogPacket(DatagramPacket packet); /** * Receive packet. * * @param p packet for receiving * @throws IOException if something goes wrong during receiving */ protected abstract void receivePacket(DatagramPacket p) throws IOException; /** * Listens for incoming datagrams, stores them for reading by the <tt>read</tt> method and * notifies the local <tt>transferHandler</tt> that there's data to be read. */ public void run() { DatagramPacket p = new DatagramPacket(buffer, 0, PACKET_RECEIVE_BUFFER_LENGTH); while (!closed) { try { // http://code.google.com/p/android/issues/detail?id=24765 if (OSUtils.IS_ANDROID) p.setLength(PACKET_RECEIVE_BUFFER_LENGTH); receivePacket(p); } catch (IOException e) { ioError = true; break; } /* * Do the DatagramPacketFilters accept the received DatagramPacket? */ DatagramPacketFilter[] datagramPacketFilters = getDatagramPacketFilters(); boolean accept; if (!enabled) accept = false; else if (datagramPacketFilters == null) accept = true; else { accept = true; for (int i = 0; i < datagramPacketFilters.length; i++) { try { if (!datagramPacketFilters[i].accept(p)) { accept = false; break; } } catch (Throwable t) { if (t instanceof ThreadDeath) throw (ThreadDeath) t; } } } if (accept) { RawPacket pkts[] = createRawPacket(p); for (int i = 0; i < pkts.length; i++) { RawPacket pkt = pkts[i]; pkts[i] = null; if (pkt != null) { if (pkt.isInvalid()) { /* * Return pkt to the pool because it is invalid and, * consequently, will not be made available to * reading. */ poolRawPacket(pkt); } else { RawPacket oldPkt; synchronized (pktSyncRoot) { oldPkt = this.pkt; this.pkt = pkt; } if (oldPkt != null) { /* * Return oldPkt to the pool because it was made * available to reading and it was not read. */ poolRawPacket(oldPkt); } if (videoRecorder != null) videoRecorder.recordData(pkt); if ((transferHandler != null) && !closed) { try { transferHandler.transferData(this); } catch (Throwable t) { /* * XXX We cannot allow transferHandler to * kill us. */ if (t instanceof ThreadDeath) { throw (ThreadDeath) t; } else { logger.warn("An RTP packet may have not been" + " fully handled.", t); } } } } } } rawPacketArrayPool.offer(pkts); } } } /** * Sets the <tt>transferHandler</tt> that this connector should be notifying when new data is * available for reading. * * @param transferHandler the <tt>transferHandler</tt> that this connector should be notifying * when new data is available for reading. */ public void setTransferHandler(SourceTransferHandler transferHandler) { if (!closed) this.transferHandler = transferHandler; } /** * Changes current thread priority. * * @param priority the new priority. */ public void setPriority(int priority) { // currently no priority is set // if (receiverThread != null) // receiverThread.setPriority(priority); } /** * Gets the <tt>DatagramPacketFilter</tt>s which allow dropping <tt>DatagramPacket</tt>s before * they are converted into <tt>RawPacket</tt>s. * * @return the <tt>DatagramPacketFilter</tt>s which allow dropping <tt>DatagramPacket</tt>s before * they are converted into <tt>RawPacket</tt>s. */ public synchronized DatagramPacketFilter[] getDatagramPacketFilters() { return datagramPacketFilters; } /** * Adds a <tt>DatagramPacketFilter</tt> which allows dropping <tt>DatagramPacket</tt>s before they * are converted into <tt>RawPacket</tt>s. * * @param datagramPacketFilter the <tt>DatagramPacketFilter</tt> which allows dropping * <tt>DatagramPacket</tt>s before they are converted into <tt>RawPacket</tt>s */ public synchronized void addDatagramPacketFilter(DatagramPacketFilter datagramPacketFilter) { if (datagramPacketFilter == null) throw new NullPointerException("datagramPacketFilter"); if (datagramPacketFilters == null) { datagramPacketFilters = new DatagramPacketFilter[] {datagramPacketFilter}; } else { final int length = datagramPacketFilters.length; for (int i = 0; i < length; i++) if (datagramPacketFilter.equals(datagramPacketFilters[i])) return; DatagramPacketFilter[] newDatagramPacketFilters = new DatagramPacketFilter[length + 1]; System.arraycopy(datagramPacketFilters, 0, newDatagramPacketFilters, 0, length); newDatagramPacketFilters[length] = datagramPacketFilter; datagramPacketFilters = newDatagramPacketFilters; } } /** * Enables or disables this <tt>RTPConnectorInputStream</tt>. While the stream is disabled, it * does not accept any packets. * * @param enabled <tt>true</tt> to enable, <tt>false</tt> to disable. */ public void setEnabled(boolean enabled) { if (logger.isDebugEnabled()) logger.debug("setEnabled: " + enabled); this.enabled = enabled; } }