1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-11-26 22:12:05 +01:00

Compare commits

...

75 commits

Author SHA1 Message Date
Florian Schmaus
11775ed6b0 Add checkstyle rule for 'synchronized' on Manager.getInstanceFor() 2019-04-15 09:58:53 +02:00
Florian Schmaus
488055948d Add missing 'synchronized' keywords to Manager.getInstanceFor()
Fixes SMACK-865.
2019-04-15 09:48:52 +02:00
Florian Schmaus
6076a9dfa5 Introduce asyncGoLimited()
which limits the number of threads created for asynchronous
operations.

Fixes SMACK-864.
2019-04-14 21:40:09 +02:00
Florian Schmaus
0ec7e84cbc Update link to XMPP Registry for Service Discovery Identities 2019-04-09 14:26:15 +02:00
Florian Schmaus
9ad162af6e Use correct field in ServiceDiscoveryManager.getIdentities()
Fixes SMACK-863.
2019-04-09 12:14:12 +02:00
Florian Schmaus
9be498c440 Fix NPE in Roster's presence listeners if 'from' is not set
The NPE is caused by an inbound presence stanza without the 'from'
attribute set. The stacktrace of the NPE is:

FATAL EXCEPTION: Smack Cached Executor
Process: de.fhg.ivi.senetz.mobile.android.mbk.debug, PID: 13365
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.Object.hashCode()' on a null object reference
    at java.util.concurrent.ConcurrentHashMap.get(ConcurrentHashMap.java:944)
    at org.jivesoftware.smack.roster.Roster.getPresencesInternal(Roster.java:374)
    at org.jivesoftware.smack.roster.Roster.getOrCreatePresencesInternal(Roster.java:388)
    at org.jivesoftware.smack.roster.Roster.access$1100(Roster.java:94)
    at org.jivesoftware.smack.roster.Roster$PresencePacketListener$1.run(Roster.java:1519)
    at org.jivesoftware.smack.AsyncButOrdered$Handler.run(AsyncButOrdered.java:121)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
    at java.lang.Thread.run(Thread.java:764)

Thanks to Marcel Heckel for reporting this.

Fixes SMACK-861.
2019-04-02 14:28:41 +02:00
Florian Schmaus
25b3f35421 Ensure that shutdown() terminates reader/writer threads
In case an exception happens in connect()/login() the
'disconnectedButResumable' boolean may (still) be set. Which causes
only one of the reader and writer threads to exit, typically the
reader thread, because shutdown() will bail out very early. This
leaves a dangling (writer) thread causing memory leaks and deadlocks
on a subsequent connect()/login().
2019-03-28 17:58:25 +01:00
Florian Schmaus
5d46e281fc XMPPTCPConnection log when reader/writer threads start and exit 2019-03-26 17:23:27 +01:00
Oliver Mihatsch
007a04c4fe Better error messages when using a Proxy to connect to the XMPP server. 2019-03-24 00:12:32 +01:00
Georg Lukas
8e88cd2e31 Proxy mode: add failed address on error 2019-03-24 00:11:01 +01:00
Oliver Mihatsch
3450ffad2b Do not use "CONNECT" in the Host header field. 2019-03-24 00:09:54 +01:00
Florian Schmaus
653d9dbba7 smack-bosh: Limit jbosh to the 0.9 series
akin to version.gradle
2019-03-22 21:55:26 +01:00
Mohsen Hariri
c83d717a26 Allow adding custom HTTP headers to bosh communications 2019-03-22 21:49:06 +01:00
Florian Schmaus
7059b60672 Wait for reader/writer thread termination at the end of shutdown()
This synchronizes the place with what the master branch currently
does.
2019-03-16 11:11:58 +01:00
Florian Schmaus
3ded023629 Remove 'synchronized' from notifyConnectionError()
as it is not neccessary and causes stalls and deadlocks with a
concurrent connect()/login() (which could be resolved interrupting the
connect()/login() calling thread):

"Smack Reader (0)" daemon prio=5 tid=21 Blocked
  | group="main" sCount=1 dsCount=0 obj=0x12c84670 self=0x7e072ca200
  | sysTid=14965 nice=0 cgrp=default sched=0/0 handle=0x7e06155450
  | state=S schedstat=( 63430626 3034269 21 ) utm=5 stm=0 core=2 HZ=100
  | stack=0x7e06053000-0x7e06055000 stackSize=1037KB
  | held mutexes=
  at org.jivesoftware.smack.tcp.XMPPTCPConnection.notifyConnectionError(XMPPTCPConnection.java:-1)
  - waiting to lock <0x0ec6e6bb> (a org.jivesoftware.smack.tcp.XMPPTCPConnection) held by thread 22
  at org.jivesoftware.smack.tcp.XMPPTCPConnection.access$3900(XMPPTCPConnection.java:154)
  at org.jivesoftware.smack.tcp.XMPPTCPConnection$PacketReader.parsePackets(XMPPTCPConnection.java:1330)
  at org.jivesoftware.smack.tcp.XMPPTCPConnection$PacketReader.access$900(XMPPTCPConnection.java:1064)
  at org.jivesoftware.smack.tcp.XMPPTCPConnection$PacketReader$1.run(XMPPTCPConnection.java:1081)
  at java.lang.Thread.run(Thread.java:761)

"connect" prio=5 tid=22 Waiting
  | group="main" sCount=1 dsCount=0 obj=0x12c8e820 self=0x7e19bcd600
  | sysTid=15047 nice=0 cgrp=default sched=0/0 handle=0x7e05ee4450
  | state=S schedstat=( 14067083 7475835 14 ) utm=1 stm=0 core=0 HZ=100
  | stack=0x7e05de2000-0x7e05de4000 stackSize=1037KB
  | held mutexes=
  at java.lang.Object.wait!(Native method)
  - waiting on <0x0c058b14> (a java.lang.Object)
  at java.lang.Thread.parkFor$(Thread.java:2127)
  - locked <0x0c058b14> (a java.lang.Object)
  at sun.misc.Unsafe.park(Unsafe.java:325)
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:161)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:840)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:994)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1303)
  at java.util.concurrent.Semaphore.acquire(Semaphore.java:446)
  at org.jivesoftware.smack.tcp.XMPPTCPConnection.initConnection(XMPPTCPConnection.java:685)
  at org.jivesoftware.smack.tcp.XMPPTCPConnection.connectInternal(XMPPTCPConnection.java:942)
  at org.jivesoftware.smack.AbstractXMPPConnection.connect(AbstractXMPPConnection.java:417)
  - locked <0x0ec6e6bb> (a org.jivesoftware.smack.tcp.XMPPTCPConnection)
  at org.yaxim.androidclient.service.SmackableImp.connectAndLogin(SmackableImp.java:717)
  - locked <0x0ec6e6bb> (a org.jivesoftware.smack.tcp.XMPPTCPConnection)
  at org.yaxim.androidclient.service.SmackableImp.doConnect(SmackableImp.java:304)
  at org.yaxim.androidclient.service.SmackableImp$5.run(SmackableImp.java:391)
2019-03-16 10:42:36 +01:00
Florian Schmaus
29af92cbd1 Smack 4.3.4-SNAPSHOT 2019-03-15 09:43:09 +01:00
Florian Schmaus
b054c4fe77 Smack 4.3.3 2019-03-14 14:31:09 +01:00
Florian Schmaus
0de0873abb version.gradle: Add link to SMACK-858 2019-03-14 14:29:23 +01:00
Georg Lukas
e5bbd19ef1 StanzaDroppedListener for XEP-0198 resumption failures
If a stream resume fails, smack will re-send all queued stanzas after a
reconnect. However, it does not make sense to re-send them:

* IQs / IQ responses have probably timed out
* MUC messages and PMs will be rejected as you haven't rejoined yet
* regular messages should be amended with a <delay> element

This patch adds a StanzaDroppedListener interface to the
XMPPTCPConnection. If at least one StanzaDroppedListener is provided,
all queued messages will be drained into the StanzaDroppedListener(s).
Otherwise, the original behavior of attempting to transmit them will be
followed.

Discussion: https://discourse.igniterealtime.org/t/xep-0198-resume-failure-reconnect-resending-of-muc-messages/83510/3

Signed-off-by: Georg Lukas <georg@op-co.de>
2019-03-10 21:24:37 +01:00
Florian Schmaus
569f7417a8 Add AuthenticatedConnectionInitiallyEstablished timestamp 2019-03-10 21:24:37 +01:00
Florian Schmaus
c4289b2c18 Add AbstractXMPPConnection.initState()
and init/reset the sychronization points there.

This method is called right at the beginning of connect() and at the
end of shutdown().
2019-03-10 21:24:37 +01:00
Florian Schmaus
7518bf9a25 Add descriptive text to StanzaError.toString() 2019-03-10 21:24:37 +01:00
Florian Schmaus
5da6dea138 Throw exception to reduce call sites of notifyConnectionError()
in XMPPTCPConnection.
2019-03-10 21:24:37 +01:00
Florian Schmaus
7d2c3ac9f9 Do not call synchronized methods in reader/writer thread
This may cause deadlocks with a call to acquire(2) on the introduced
readerWriterSemaphore in initConnection(), which is also synchronized.
2019-03-10 21:24:37 +01:00
Florian Schmaus
3d1a781a22 Show correct reply timeout value in StanzaCollector's NoResponseException 2019-03-05 08:21:59 +01:00
Florian Schmaus
7f0932a481 Reset the MUC self-presence collector on presence stanzas on join
To prevent timeouts when joining very large MUCs we now reset the
self-presence collector's timeout for every other (occupant) presence
we receive.

Fixes SMACK-859.
2019-03-04 23:01:52 +01:00
Florian Schmaus
f602de8771 Call shutdown() in connect() on exception
to clean up the state build up by connect().

Related to SMACK-855 there is the possiblitiy of a stray (writer)
thread if, for example, tlsHandled.checkifSuccessOrWaitorThrow() in
XMPPTCPConnection.connectInternal() throws. This commit should prevent
that.
2019-03-04 20:14:12 +01:00
Florian Schmaus
f4ebd530e6 Add note about module dependencies to JavaxResolver
Related to SMACK-856.
2019-03-02 14:47:28 +01:00
Florian Schmaus
78ee22c261 Revert "Do not set com.sun.jndi.dns.DnsContextFactory in JavaxResolver"
This reverts commit ac9641f091.

Reverted because now an NoInitialContextException is now thrown.

Related to SMACK-856.
2019-03-02 14:42:19 +01:00
Florian Schmaus
456d645e27 Use different version specifier for jxmpp and MiniDNS
Fixes SMACK-858.
2019-02-24 22:09:47 +01:00
Florian Schmaus
3bdc1d30b1 Correctly name provider INSTANCE fields
and make them 'final' where possible.
2019-02-23 23:59:17 +01:00
Florian Schmaus
5f7cfd04bd Add further unit test to StableUniqueStanzaIdTest 2019-02-23 23:59:17 +01:00
Florian Schmaus
5a2109e73f Move cast in extra line in ADD_ORIGIN_ID_INTERCEPTOR 2019-02-23 23:40:27 +01:00
Florian Schmaus
4da4558b29 Make origin-id interceptor static and rename it 2019-02-23 23:39:58 +01:00
Florian Schmaus
27749b2137 Remove unused filter in StableUniqueStanzaIdManager 2019-02-23 23:39:41 +01:00
Florian Schmaus
47f76952e3 Smack 4.3.3-SNAPSHOT 2019-02-22 10:57:48 +01:00
Florian Schmaus
a8044f723a Smack 4.3.2 2019-02-22 09:22:23 +01:00
Florian Schmaus
e9b514548b Filter error messages in DeliveryReceiptManager 2019-02-17 22:27:38 +01:00
Florian Schmaus
db8e37d75f Reduce scope of catched Exceptions in JavaxResolver 2019-02-17 22:27:38 +01:00
Florian Schmaus
eccaf58df1 Add Logger to JavaxResolver 2019-02-17 22:27:38 +01:00
Florian Schmaus
ac9641f091 Do not set com.sun.jndi.dns.DnsContextFactory in JavaxResolver
as this is not accessible in newer Java versions (at least 11).

Fixes SMACK-856.
2019-02-17 22:27:38 +01:00
Florian Schmaus
78dcaec75b Remove null checks for writer/reader fields in XMPPTCPConnection
as those are never null since
60f324eb1b (the previous commit).
2019-02-09 18:20:55 +01:00
Florian Schmaus
60f324eb1b Make writer/reader fields final in XMPPTCPConnection 2019-02-09 18:12:36 +01:00
Florian Schmaus
09bffb8dca Fail sync points on exception in XMPPTCPConnection 2019-02-09 18:12:03 +01:00
Florian Schmaus
5c8e830157 Log if not all reader/writer threads where terminated 2019-02-09 18:12:01 +01:00
Florian Schmaus
62cba0d96f XMPPTCPConnection: Ensure both writer/reader threads are terminated
This should fix SMACK-855.
2019-02-09 13:36:42 +01:00
Florian Schmaus
5273402b87 Use ranged versions for jXMPP and MiniDNS 2018-12-30 16:30:47 +01:00
Florian Schmaus
1a4ad7b8b5 Mark Local SOCKS5 Proxy thread as daemon thread
Fixes SMACK-849.
2018-12-27 14:42:48 +01:00
Florian Schmaus
4b21f003af Add MultiUserChat.leaveSync()
Fixes SMACK-848.
2018-12-26 21:39:25 +01:00
Florian Schmaus
b9c12d44c3 Use InetSocketAddress in log message in XMPPTCPConnection
The inetAddressAndPort String is redundant since
a2743549b8, because we now construct the
InetSocketAddress earlier and can hence use it in the log statement.
2018-12-21 12:09:03 +01:00
Florian Schmaus
a2743549b8 Make TCP socket connection attempt interruptable
by introducing SmackFuture.SocketFuture.

Fixes SMACK-847.
2018-12-21 12:03:49 +01:00
Florian Schmaus
d1c73eba8d Fix MamManager javadoc
of methods which have been renamed.
2018-12-20 17:00:00 +01:00
Florian Schmaus
48e3127172 Always wait for stream features after authentication
The purpose of the "maybeCompressFeaturesReceived" synchronization
point is to wait for the stream features after authentication. This is
not really reflected by its name and furthermore its
checkIfSuccessOrWait() method was only invoked if compression was
enabled, but we always want to wait for the stream features after
authentication. Hence move the call before the isCompressionEnabled()
check and one layer up in the call stack.

Fixes SMACK-846.
2018-12-20 16:52:44 +01:00
Florian Schmaus
f6da386dea Ensure that IQ response 'to' address and ID are set correctly
Fixes SMACK-845.
2018-12-14 17:18:53 +01:00
Florian Schmaus
5ddaa623da Check if unacknowledged stanzas queue is full before adding to it
to avoid an IllegalStateException.

Fixes SMACK-844.
2018-12-08 22:54:13 +01:00
Florian Schmaus
0c134db072 Add ConnectionConfiguration.setHostAddressByNameOrIp(CharSequence fqdnOrIp) 2018-12-04 15:59:11 +01:00
Tairs Rzajevs
0332fa54d1 Fix previous archive page requested incorrectly in MamManager
Previous page should be before the first message in the previous
result set, not the last.

Fixes SMACK-843.
2018-11-29 22:50:59 +01:00
Florian Schmaus
fa7297019d Add checkstyle rule for "Boolean.valueOf()" usages 2018-11-29 22:38:11 +01:00
Florian Schmaus
dbfc123e5e Use ParserUtils.parseXmlBoolean() where appropriate 2018-11-29 22:29:21 +01:00
Florian Schmaus
7ea7f9e2e9 Add ParserUtils.parseXmlBoolean(String) 2018-11-29 09:02:05 +01:00
Florian Schmaus
b675f49b3d
Merge pull request #287 from ge0rg/4.3
Partially fix boolean parser, 4.3 branch
2018-11-28 13:57:22 +01:00
Georg Lukas
b8bd10b056 RoomInfo: use proper boolean parser for muc#roominfo_subjectmod
XML allows both false/true and 0/1 syntax for booleans.

Signed-off-by: Georg Lukas <georg@op-co.de>
2018-11-28 11:11:52 +01:00
Georg Lukas
8b88f9cb20 Bookmarks: use proper boolean parser for autojoin
Some clients (read: Gajim) store boolean values as `0` and `1` instead
of `false` and `true`, which is legal for the XML boolean type.

Signed-off-by: Georg Lukas <georg@op-co.de>
2018-11-28 11:11:52 +01:00
Georg Lukas
229653af30 ParserUtils: fix boolean parser
How could this even happen?

Signed-off-by: Georg Lukas <georg@op-co.de>
2018-11-28 11:11:52 +01:00
asokolov
2900c5ae23 Move xml-not-well-formed (RFC 3920) condition handling to StreamError
Fixes SMACK-842.
2018-11-14 14:53:04 +01:00
Florian Schmaus
5f05da2f77 Smack 4.3.2-SNAPSHOT 2018-10-14 14:22:00 +02:00
Florian Schmaus
6a43481320 Smack 4.3.1 2018-10-14 12:53:11 +02:00
Florian Schmaus
cf22371d3e Catch IllegalArgumentException in XmlUtil clinit
Fixes SMACK-833.
2018-10-14 12:10:50 +02:00
Florian Schmaus
ec982f65e2 Fix IndexOutOfBoundsException in FormField.getFirstValue()
Fixes SMACK-838.
2018-10-14 12:07:51 +02:00
Florian Schmaus
89c9a41863 Bump MiniDNS to 0.3.3 2018-10-14 11:57:50 +02:00
Florian Schmaus
85731fbe3e
Merge pull request #275 from spslinger/fix-getPresence-ConcurrentModificationException
Fix getPresence ConcurrentModificationException
2018-10-11 14:11:02 +02:00
spslinger
1e21ab763c
Fix getPresence ConcurrentModificationException
Fix for SMACK-841

Since Smack 4.2.4, the getPresencesInternal method in the Roster class
can return a LruCache object, which is a LinkedHashMap with access
order. This means that any access using get or getOrDefault will be a
modification of the Map. If you loop over the keySet of the Map and
there are more than one, the second call to get will throw a
ConcurrentModificationException!

Since the keys are only used here to obtain the corresponding
values, the simplest solution is to just loop over the values instead.
2018-10-11 12:29:32 +02:00
Florian Schmaus
1aa35bc957 Merge branch '4.3' of github.com:Flowdalic/Smack into 4.3 2018-09-12 20:02:16 +02:00
Florian Schmaus
87fac888c6 Improve MultiUserChat's API to query hosted rooms
Return a Map instead of a List. This makes it possible to check for
the existence of MUC by looking up the MUC's address in the Map's key
set.
2018-09-12 19:56:46 +02:00
Florian Schmaus
77baaa99bc Do not warn on presence from bare MUC JID in MultiUserChat
as such presence messages became a thing in XMPP (e.g. for XEP-0153
avatars for MUC).
2018-09-10 21:00:49 +02:00
54 changed files with 1120 additions and 173 deletions

View file

@ -6,6 +6,11 @@
<module name="SuppressionFilter"> <module name="SuppressionFilter">
<property name="file" value="config/suppressions.xml"/> <property name="file" value="config/suppressions.xml"/>
</module> </module>
<module name="SuppressWithPlainTextCommentFilter">
<property name="offCommentFormat" value="CHECKSTYLE\:OFF\:(\w+)"/>
<property name="onCommentFormat" value="CHECKSTYLE\:ON\:(\w+)"/>
<property name="checkFormat" value="$1"/>
</module>
<module name="Header"> <module name="Header">
<property name="headerFile" value="config/${checkstyleLicenseHeader}.txt"/> <property name="headerFile" value="config/${checkstyleLicenseHeader}.txt"/>
<property name="ignoreLines" value="3"/> <property name="ignoreLines" value="3"/>
@ -61,6 +66,14 @@
<property name="format" value="^\s*//[^\s]"/> <property name="format" value="^\s*//[^\s]"/>
<property name="message" value="Comment start ('//') followed by non-space character. You would not continue after a punctuation without a space, would you?"/> <property name="message" value="Comment start ('//') followed by non-space character. You would not continue after a punctuation without a space, would you?"/>
</module> </module>
<!-- Check for synchronized keyword on Manager's static
getInstanceFor() method. Note that if XMPPConnection is every
replaced with something else, then we need to change it here
too. -->
<module name="RegexpSingleline">
<property name="format" value="^\s*public(?!.*synchronized).*getInstanceFor\(XMPPConnection.*$"/>
<property name="message" value="getInstanceFor() should be synchronized"/>
</module>
<module name="JavadocPackage"/> <module name="JavadocPackage"/>
<module name="TreeWalker"> <module name="TreeWalker">
<module name="SuppressionCommentFilter"/> <module name="SuppressionCommentFilter"/>
@ -89,6 +102,11 @@
<property name="message" value="Usage of println"/> <property name="message" value="Usage of println"/>
<property name="ignoreComments" value="true"/> <property name="ignoreComments" value="true"/>
</module> </module>
<module name="RegexpSinglelineJava">
<property name="format" value="Boolean\.valueOf\("/>
<property name="message" value="Usage Boolean.valueOf(), consider using ParserUtils.parseXmlBoolean() instead (if you want to parse xs:boolean values)"/>
<property name="ignoreComments" value="true"/>
</module>
<module name="RegexpSinglelineJava"> <module name="RegexpSinglelineJava">
<property name="format" value="^\t+"/> <property name="format" value="^\t+"/>
<property name="message" value="Indent must not use tab characters. Use space instead."/> <property name="message" value="Indent must not use tab characters. Use space instead."/>

View file

@ -141,6 +141,74 @@ hr {
<div id="pageBody"> <div id="pageBody">
<h2>4.3.3 -- <span style="font-weight: normal;">2019-03-14</span></h2>
<h2> Bug
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-856'>SMACK-856</a>] - Smack fails under JDK 11 because com.sun.jndi.dns.DnsContextFactory is not inaccessible
</li>
</ul>
<h2> Improvement
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-858'>SMACK-858</a>] - Dependency version specifier of jxmpp and MiniDNS include alpha/beta/... versions of the follow up version when Maven is used
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-859'>SMACK-859</a>] - MultiUserChat enter() should reset the timeout of the collector waiting for the final self presence to prevent timeouts for large MUCs
</li>
</ul>
<h2>4.3.2 -- <span style="font-weight: normal;">2019-02-22</span></h2>
<h2> Bug
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-842'>SMACK-842</a>] - The RFC 3920 xml-not-well-formed error condition should be handled in stream error not a stanza error
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-843'>SMACK-843</a>] - ManManager.pagePrevious() pages into the wrong direction
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-844'>SMACK-844</a>] - Check if bounded unacknowledged stanzas queue is full before adding to it to avoid IllegalStateException
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-845'>SMACK-845</a>] - Ensure that IQ response &#39;to&#39; address and ID are set correctly
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-846'>SMACK-846</a>] - XMPPTCPConnection does not wait for stream features after authentication if compression is disabled
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-848'>SMACK-848</a>] - Make MultiUserChat.leave() wait for response
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-850'>SMACK-850</a>] - DeliveryReceiptManager should not send receipts with messages of type &#39;groupchat&#39;
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-855'>SMACK-855</a>] - XMPPTCPConnection sometimes has two writer threads running
</li>
</ul>
<h2> Improvement
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-847'>SMACK-847</a>] - Make TCP socket connection attempt interruptable
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-849'>SMACK-849</a>] - Smack Local SOCKS5 Proxy thread should be marked as daemon thread
</li>
</ul>
<h2>4.3.1 -- <span style="font-weight: normal;">2018-10-14</span></h2>
<h2> Bug
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-833'>SMACK-833</a>] - XMLUtil.prettyFormatXml() throws on some Android devices
</li>
</ul>
<h2> Improvement
</h2>
<ul>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-829'>SMACK-829</a>] - Disconnect BOSH client on shutdown
</li>
<li>[<a href='https://issues.igniterealtime.org/browse/SMACK-838'>SMACK-838</a>] - FormField.getFirstValue() throws IndexOutOfBoundsException if there are no values
</li>
</ul>
<h2>4.3.0 -- <span style="font-weight: normal;">2018-08-02</span></h2> <h2>4.3.0 -- <span style="font-weight: normal;">2018-08-02</span></h2>
<h2> Bug <h2> Bug

View file

@ -4,5 +4,7 @@ This API is considered beta quality."""
dependencies { dependencies {
compile project(':smack-core') compile project(':smack-core')
compile 'org.igniterealtime.jbosh:jbosh:[0.9,0.10)' // See https://issues.igniterealtime.org/browse/SMACK-858 and
// comment in version.gradle why the specify the version this way.
compile 'org.igniterealtime.jbosh:jbosh:[0.9.1,0.9.999]'
} }

View file

@ -19,6 +19,8 @@ package org.jivesoftware.smack.bosh;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.proxy.ProxyInfo; import org.jivesoftware.smack.proxy.ProxyInfo;
@ -34,6 +36,7 @@ public final class BOSHConfiguration extends ConnectionConfiguration {
private final boolean https; private final boolean https;
private final String file; private final String file;
private Map<String, String> httpHeaders;
private BOSHConfiguration(Builder builder) { private BOSHConfiguration(Builder builder) {
super(builder); super(builder);
@ -49,6 +52,7 @@ public final class BOSHConfiguration extends ConnectionConfiguration {
} else { } else {
file = builder.file; file = builder.file;
} }
httpHeaders = builder.httpHeaders;
} }
public boolean isProxyEnabled() { public boolean isProxyEnabled() {
@ -76,6 +80,10 @@ public final class BOSHConfiguration extends ConnectionConfiguration {
return new URI((https ? "https://" : "http://") + this.host + ":" + this.port + file); return new URI((https ? "https://" : "http://") + this.host + ":" + this.port + file);
} }
public Map<String, String> getHttpHeaders() {
return httpHeaders;
}
public static Builder builder() { public static Builder builder() {
return new Builder(); return new Builder();
} }
@ -83,6 +91,7 @@ public final class BOSHConfiguration extends ConnectionConfiguration {
public static final class Builder extends ConnectionConfiguration.Builder<Builder, BOSHConfiguration> { public static final class Builder extends ConnectionConfiguration.Builder<Builder, BOSHConfiguration> {
private boolean https; private boolean https;
private String file; private String file;
private Map<String, String> httpHeaders = new HashMap<>();
private Builder() { private Builder() {
} }
@ -101,6 +110,11 @@ public final class BOSHConfiguration extends ConnectionConfiguration {
return this; return this;
} }
public Builder addHttpHeader(String name, String value) {
httpHeaders.put(name, value);
return this;
}
@Override @Override
public BOSHConfiguration build() { public BOSHConfiguration build() {
return new BOSHConfiguration(this); return new BOSHConfiguration(this);

View file

@ -22,6 +22,7 @@ import java.io.PipedReader;
import java.io.PipedWriter; import java.io.PipedWriter;
import java.io.StringReader; import java.io.StringReader;
import java.io.Writer; import java.io.Writer;
import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -156,6 +157,9 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
if (config.isProxyEnabled()) { if (config.isProxyEnabled()) {
cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort()); cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
} }
for (Map.Entry<String, String> h : config.getHttpHeaders().entrySet()) {
cfgBuilder.addHttpHeader(h.getKey(), h.getValue());
}
client = BOSHClient.create(cfgBuilder.build()); client = BOSHClient.create(cfgBuilder.build());
client.addBOSHClientConnListener(new BOSHConnectionListener()); client.addBOSHClientConnListener(new BOSHConnectionListener());
@ -263,6 +267,11 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
client = null; client = null;
} }
instantShutdown();
}
@Override
public void instantShutdown() {
setWasAuthenticated(); setWasAuthenticated();
sessionID = null; sessionID = null;
done = true; done = true;

View file

@ -26,6 +26,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
@ -235,7 +236,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
* stanza is send by the server. This is set to true once the last feature stanza has been * stanza is send by the server. This is set to true once the last feature stanza has been
* parsed. * parsed.
*/ */
protected final SynchronizationPoint<Exception> lastFeaturesReceived = new SynchronizationPoint<Exception>( protected final SynchronizationPoint<SmackException> lastFeaturesReceived = new SynchronizationPoint<>(
AbstractXMPPConnection.this, "last stream features received from server"); AbstractXMPPConnection.this, "last stream features received from server");
/** /**
@ -297,7 +298,18 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
} }
}); });
private static final AsyncButOrdered<AbstractXMPPConnection> ASYNC_BUT_ORDERED = new AsyncButOrdered<>(); protected static final AsyncButOrdered<AbstractXMPPConnection> ASYNC_BUT_ORDERED = new AsyncButOrdered<>();
/**
* An executor which uses {@link #asyncGoLimited(Runnable)} to limit the number of asynchronously processed runnables
* per connection.
*/
private final Executor limitedExcutor = new Executor() {
@Override
public void execute(Runnable runnable) {
asyncGoLimited(runnable);
}
};
/** /**
* The used host to establish the connection to * The used host to establish the connection to
@ -314,6 +326,9 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
*/ */
protected boolean authenticated = false; protected boolean authenticated = false;
// TODO: Migrate to ZonedDateTime once Smack's minimum required Android SDK level is 26 (8.0, Oreo) or higher.
protected long authenticatedConnectionInitiallyEstablishedTimestamp;
/** /**
* Flag that indicates if the user was authenticated with the server when the connection * Flag that indicates if the user was authenticated with the server when the connection
* to the server was closed (abruptly or not). * to the server was closed (abruptly or not).
@ -381,6 +396,12 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
@Override @Override
public abstract boolean isUsingCompression(); public abstract boolean isUsingCompression();
protected void initState() {
saslFeatureReceived.init();
lastFeaturesReceived.init();
tlsHandled.init();
}
/** /**
* Establishes a connection to the XMPP server. It basically * Establishes a connection to the XMPP server. It basically
* creates and maintains a connection to the server. * creates and maintains a connection to the server.
@ -399,12 +420,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
throwAlreadyConnectedExceptionIfAppropriate(); throwAlreadyConnectedExceptionIfAppropriate();
// Reset the connection state // Reset the connection state
initState();
saslAuthentication.init(); saslAuthentication.init();
saslFeatureReceived.init();
lastFeaturesReceived.init();
tlsHandled.init();
streamId = null; streamId = null;
try {
// Perform the actual connection to the XMPP service // Perform the actual connection to the XMPP service
connectInternal(); connectInternal();
@ -412,9 +432,12 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
// from the server and throw an error. First check if we've already negotiated TLS // from the server and throw an error. First check if we've already negotiated TLS
// and are secure, however (features get parsed a second time after TLS is established). // and are secure, however (features get parsed a second time after TLS is established).
if (!isSecureConnection() && getConfiguration().getSecurityMode() == SecurityMode.required) { if (!isSecureConnection() && getConfiguration().getSecurityMode() == SecurityMode.required) {
shutdown();
throw new SecurityRequiredByClientException(); throw new SecurityRequiredByClientException();
} }
} catch (SmackException | IOException | XMPPException | InterruptedException e) {
instantShutdown();
throw e;
}
// Make note of the fact that we're now connected. // Make note of the fact that we're now connected.
connected = true; connected = true;
@ -550,7 +573,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
// - the servers last features stanza has been parsed // - the servers last features stanza has been parsed
// - the timeout occurs // - the timeout occurs
LOGGER.finer("Waiting for last features to be received before continuing with resource binding"); LOGGER.finer("Waiting for last features to be received before continuing with resource binding");
lastFeaturesReceived.checkIfSuccessOrWait(); lastFeaturesReceived.checkIfSuccessOrWaitOrThrow();
if (!hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) { if (!hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) {
@ -582,6 +605,9 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
} }
protected void afterSuccessfulLogin(final boolean resumed) throws NotConnectedException, InterruptedException { protected void afterSuccessfulLogin(final boolean resumed) throws NotConnectedException, InterruptedException {
if (!resumed) {
authenticatedConnectionInitiallyEstablishedTimestamp = System.currentTimeMillis();
}
// Indicate that we're now authenticated. // Indicate that we're now authenticated.
this.authenticated = true; this.authenticated = true;
@ -763,6 +789,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
*/ */
protected abstract void shutdown(); protected abstract void shutdown();
/**
* Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza.
*/
public abstract void instantShutdown();
@Override @Override
public void addConnectionListener(ConnectionListener connectionListener) { public void addConnectionListener(ConnectionListener connectionListener) {
if (connectionListener == null) { if (connectionListener == null) {
@ -1097,6 +1128,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
if (packet instanceof IQ) { if (packet instanceof IQ) {
final IQ iq = (IQ) packet; final IQ iq = (IQ) packet;
if (iq.isRequestIQ()) { if (iq.isRequestIQ()) {
final IQ iqRequest = iq;
final String key = XmppStringUtils.generateKey(iq.getChildElementName(), iq.getChildElementNamespace()); final String key = XmppStringUtils.generateKey(iq.getChildElementName(), iq.getChildElementNamespace());
IQRequestHandler iqRequestHandler; IQRequestHandler iqRequestHandler;
final IQ.Type type = iq.getType(); final IQ.Type type = iq.getType();
@ -1146,7 +1178,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
executorService = ASYNC_BUT_ORDERED.asExecutorFor(this); executorService = ASYNC_BUT_ORDERED.asExecutorFor(this);
break; break;
case async: case async:
executorService = CACHED_EXECUTOR_SERVICE; executorService = limitedExcutor;
break; break;
} }
final IQRequestHandler finalIqRequestHandler = iqRequestHandler; final IQRequestHandler finalIqRequestHandler = iqRequestHandler;
@ -1162,6 +1194,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
// e.g. to avoid presence leaks. // e.g. to avoid presence leaks.
return; return;
} }
assert (response.getType() == IQ.Type.result || response.getType() == IQ.Type.error);
response.setTo(iqRequest.getFrom());
response.setStanzaId(iqRequest.getStanzaId());
try { try {
sendStanza(response); sendStanza(response);
} }
@ -1191,7 +1228,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
} }
for (final StanzaListener listener : listenersToNotify) { for (final StanzaListener listener : listenersToNotify) {
asyncGo(new Runnable() { asyncGoLimited(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
@ -1710,6 +1747,18 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
return lastStanzaReceived; return lastStanzaReceived;
} }
/**
* Get the timestamp when the connection was the first time authenticated, i.e., when the first successful login was
* performed. Note that this value is not reset on disconnect, so it represents the timestamp from the last
* authenticated connection. The value is also not reset on stream resumption.
*
* @return the timestamp or {@code null}.
* @since 4.3.3
*/
public final long getAuthenticatedConnectionInitiallyEstablishedTimestamp() {
return authenticatedConnectionInitiallyEstablishedTimestamp;
}
/** /**
* Install a parsing exception callback, which will be invoked once an exception is encountered while parsing a * Install a parsing exception callback, which will be invoked once an exception is encountered while parsing a
* stanza. * stanza.
@ -1736,6 +1785,75 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
return getClass().getSimpleName() + '[' + localEndpointString + "] (" + getConnectionCounter() + ')'; return getClass().getSimpleName() + '[' + localEndpointString + "] (" + getConnectionCounter() + ')';
} }
/**
* A queue of deferred runnables that where not executed immediately because {@link #currentAsyncRunnables} reached
* {@link #maxAsyncRunnables}. Note that we use a {@code LinkedList} in order to avoid space blowups in case the
* list ever becomes very big and shrinks again.
*/
private final Queue<Runnable> deferredAsyncRunnables = new LinkedList<>();
private int deferredAsyncRunnablesCount;
private int deferredAsyncRunnablesCountPrevious;
private int maxAsyncRunnables = SmackConfiguration.getDefaultConcurrencyLevelLimit();
private int currentAsyncRunnables;
protected void asyncGoLimited(final Runnable runnable) {
Runnable wrappedRunnable = new Runnable() {
@Override
public void run() {
runnable.run();
synchronized (deferredAsyncRunnables) {
Runnable defferredRunnable = deferredAsyncRunnables.poll();
if (defferredRunnable == null) {
currentAsyncRunnables--;
} else {
deferredAsyncRunnablesCount--;
asyncGo(defferredRunnable);
}
}
}
};
synchronized (deferredAsyncRunnables) {
if (currentAsyncRunnables < maxAsyncRunnables) {
currentAsyncRunnables++;
asyncGo(wrappedRunnable);
} else {
deferredAsyncRunnablesCount++;
deferredAsyncRunnables.add(wrappedRunnable);
}
final int HIGH_WATERMARK = 100;
final int INFORM_WATERMARK = 20;
final int deferredAsyncRunnablesCount = this.deferredAsyncRunnablesCount;
if (deferredAsyncRunnablesCount >= HIGH_WATERMARK
&& deferredAsyncRunnablesCountPrevious < HIGH_WATERMARK) {
LOGGER.log(Level.WARNING, "High watermark of " + HIGH_WATERMARK + " simultaneous executing runnables reached");
} else if (deferredAsyncRunnablesCount >= INFORM_WATERMARK
&& deferredAsyncRunnablesCountPrevious < INFORM_WATERMARK) {
LOGGER.log(Level.INFO, INFORM_WATERMARK + " simultaneous executing runnables reached");
}
deferredAsyncRunnablesCountPrevious = deferredAsyncRunnablesCount;
}
}
public void setMaxAsyncOperations(int maxAsyncOperations) {
if (maxAsyncOperations < 1) {
throw new IllegalArgumentException("Max async operations must be greater than 0");
}
synchronized (deferredAsyncRunnables) {
maxAsyncRunnables = maxAsyncOperations;
}
}
protected static void asyncGo(Runnable runnable) { protected static void asyncGo(Runnable runnable) {
CACHED_EXECUTOR_SERVICE.execute(runnable); CACHED_EXECUTOR_SERVICE.execute(runnable);
} }

View file

@ -55,6 +55,16 @@ public class AsyncButOrdered<K> {
private final Map<K, Boolean> threadActiveMap = new WeakHashMap<>(); private final Map<K, Boolean> threadActiveMap = new WeakHashMap<>();
private final Executor executor;
public AsyncButOrdered() {
this(null);
}
public AsyncButOrdered(Executor executor) {
this.executor = executor;
}
/** /**
* Invoke the given {@link Runnable} asynchronous but ordered in respect to the given key. * Invoke the given {@link Runnable} asynchronous but ordered in respect to the given key.
* *
@ -86,7 +96,11 @@ public class AsyncButOrdered<K> {
if (newHandler) { if (newHandler) {
Handler handler = new Handler(keyQueue, key); Handler handler = new Handler(keyQueue, key);
threadActiveMap.put(key, true); threadActiveMap.put(key, true);
if (executor == null) {
AbstractXMPPConnection.asyncGo(handler); AbstractXMPPConnection.asyncGo(handler);
} else {
executor.execute(handler);
}
} }
} }

View file

@ -18,6 +18,7 @@
package org.jivesoftware.smack; package org.jivesoftware.smack;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.KeyStore; import java.security.KeyStore;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -45,6 +46,7 @@ import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.stringprep.XmppStringprepException; import org.jxmpp.stringprep.XmppStringprepException;
import org.minidns.dnsname.DnsName; import org.minidns.dnsname.DnsName;
import org.minidns.util.InetAddressUtil;
/** /**
* Configuration to use while establishing the connection to the server. * Configuration to use while establishing the connection to the server.
@ -171,6 +173,14 @@ public abstract class ConnectionConfiguration {
} }
DnsName getHost() {
return host;
}
InetAddress getHostAddress() {
return hostAddress;
}
/** /**
* Returns the server name of the target server. * Returns the server name of the target server.
* *
@ -642,6 +652,33 @@ public abstract class ConnectionConfiguration {
return getThis(); return getThis();
} }
/**
* Set the host to connect to by either its fully qualified domain name (FQDN) or its IP.
*
* @param fqdnOrIp a CharSequence either representing the FQDN or the IP of the host.
* @return a reference to this builder.
* @see #setHost(DnsName)
* @see #setHostAddress(InetAddress)
* @since 4.3.2
*/
public B setHostAddressByNameOrIp(CharSequence fqdnOrIp) {
String fqdnOrIpString = fqdnOrIp.toString();
if (InetAddressUtil.isIpAddress(fqdnOrIp)) {
InetAddress hostInetAddress;
try {
hostInetAddress = InetAddress.getByName(fqdnOrIpString);
}
catch (UnknownHostException e) {
// Should never happen.
throw new AssertionError(e);
}
setHostAddress(hostInetAddress);
} else {
setHost(fqdnOrIpString);
}
return getThis();
}
public B setPort(int port) { public B setPort(int port) {
if (port < 0 || port > 65535) { if (port < 0 || port > 65535) {
throw new IllegalArgumentException( throw new IllegalArgumentException(

View file

@ -365,4 +365,19 @@ public final class SmackConfiguration {
public static void setUnknownIqRequestReplyMode(UnknownIqRequestReplyMode unknownIqRequestReplyMode) { public static void setUnknownIqRequestReplyMode(UnknownIqRequestReplyMode unknownIqRequestReplyMode) {
SmackConfiguration.unknownIqRequestReplyMode = Objects.requireNonNull(unknownIqRequestReplyMode, "Must set mode"); SmackConfiguration.unknownIqRequestReplyMode = Objects.requireNonNull(unknownIqRequestReplyMode, "Must set mode");
} }
private static final int defaultConcurrencyLevelLimit;
static {
int availableProcessors = Runtime.getRuntime().availableProcessors();
if (availableProcessors < 8) {
defaultConcurrencyLevelLimit = 8;
} else {
defaultConcurrencyLevelLimit = (int) (availableProcessors * 1.1);
}
}
public static int getDefaultConcurrencyLevelLimit() {
return defaultConcurrencyLevelLimit;
}
} }

View file

@ -93,13 +93,24 @@ public class SmackException extends Exception {
return new NoResponseException(sb.toString()); return new NoResponseException(sb.toString());
} }
@Deprecated
// TODO: Remove in Smack 4.4.
public static NoResponseException newWith(XMPPConnection connection, public static NoResponseException newWith(XMPPConnection connection,
StanzaCollector collector) { StanzaCollector collector) {
return newWith(connection, collector.getStanzaFilter()); return newWith(connection, collector.getStanzaFilter());
} }
public static NoResponseException newWith(long timeout,
StanzaCollector collector) {
return newWith(timeout, collector.getStanzaFilter());
}
public static NoResponseException newWith(XMPPConnection connection, StanzaFilter filter) { public static NoResponseException newWith(XMPPConnection connection, StanzaFilter filter) {
final StringBuilder sb = getWaitingFor(connection); return newWith(connection.getReplyTimeout(), filter);
}
public static NoResponseException newWith(long timeout, StanzaFilter filter) {
final StringBuilder sb = getWaitingFor(timeout);
sb.append(" Waited for response using: "); sb.append(" Waited for response using: ");
if (filter != null) { if (filter != null) {
sb.append(filter.toString()); sb.append(filter.toString());
@ -112,7 +123,10 @@ public class SmackException extends Exception {
} }
private static StringBuilder getWaitingFor(XMPPConnection connection) { private static StringBuilder getWaitingFor(XMPPConnection connection) {
final long replyTimeout = connection.getReplyTimeout(); return getWaitingFor(connection.getReplyTimeout());
}
private static StringBuilder getWaitingFor(final long replyTimeout) {
final StringBuilder sb = new StringBuilder(256); final StringBuilder sb = new StringBuilder(256);
sb.append("No response received within reply timeout. Timeout was " sb.append("No response received within reply timeout. Timeout was "
+ replyTimeout + "ms (~" + replyTimeout + "ms (~"
@ -334,4 +348,16 @@ public class SmackException extends Exception {
super("Resource binding was not offered by server"); super("Resource binding was not offered by server");
} }
} }
public static class SmackWrappedException extends SmackException {
/**
*
*/
private static final long serialVersionUID = 1L;
public SmackWrappedException(Exception exception) {
super(exception);
}
}
} }

View file

@ -16,11 +16,18 @@
*/ */
package org.jivesoftware.smack; package org.jivesoftware.smack;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.SocketFactory;
import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.util.CallbackRecipient; import org.jivesoftware.smack.util.CallbackRecipient;
@ -29,6 +36,8 @@ import org.jivesoftware.smack.util.SuccessCallback;
public abstract class SmackFuture<V, E extends Exception> implements Future<V>, CallbackRecipient<V, E> { public abstract class SmackFuture<V, E extends Exception> implements Future<V>, CallbackRecipient<V, E> {
private static final Logger LOGGER = Logger.getLogger(SmackFuture.class.getName());
private boolean cancelled; private boolean cancelled;
protected V result; protected V result;
@ -94,7 +103,7 @@ public abstract class SmackFuture<V, E extends Exception> implements Future<V>,
@Override @Override
public final synchronized V get() throws InterruptedException, ExecutionException { public final synchronized V get() throws InterruptedException, ExecutionException {
while (result == null && exception == null && !cancelled) { while (result == null && exception == null && !cancelled) {
wait(); futureWait();
} }
return getOrThrowExecutionException(); return getOrThrowExecutionException();
@ -102,7 +111,7 @@ public abstract class SmackFuture<V, E extends Exception> implements Future<V>,
public final synchronized V getOrThrow() throws E, InterruptedException { public final synchronized V getOrThrow() throws E, InterruptedException {
while (result == null && exception == null && !cancelled) { while (result == null && exception == null && !cancelled) {
wait(); futureWait();
} }
if (exception != null) { if (exception != null) {
@ -124,7 +133,7 @@ public abstract class SmackFuture<V, E extends Exception> implements Future<V>,
while (result != null && exception != null) { while (result != null && exception != null) {
final long waitTimeRemaining = deadline - System.currentTimeMillis(); final long waitTimeRemaining = deadline - System.currentTimeMillis();
if (waitTimeRemaining > 0) { if (waitTimeRemaining > 0) {
wait(waitTimeRemaining); futureWait(waitTimeRemaining);
} }
} }
@ -162,6 +171,15 @@ public abstract class SmackFuture<V, E extends Exception> implements Future<V>,
} }
} }
protected final void futureWait() throws InterruptedException {
futureWait(0);
}
@SuppressWarnings("WaitNotInLoop")
protected void futureWait(long timeout) throws InterruptedException {
wait(timeout);
}
public static class InternalSmackFuture<V, E extends Exception> extends SmackFuture<V, E> { public static class InternalSmackFuture<V, E extends Exception> extends SmackFuture<V, E> {
public final synchronized void setResult(V result) { public final synchronized void setResult(V result) {
this.result = result; this.result = result;
@ -178,6 +196,64 @@ public abstract class SmackFuture<V, E extends Exception> implements Future<V>,
} }
} }
public static class SocketFuture extends InternalSmackFuture<Socket, IOException> {
private final Socket socket;
private final Object wasInterruptedLock = new Object();
private boolean wasInterrupted;
public SocketFuture(SocketFactory socketFactory) throws IOException {
socket = socketFactory.createSocket();
}
@Override
protected void futureWait(long timeout) throws InterruptedException {
try {
super.futureWait(timeout);
} catch (InterruptedException interruptedException) {
synchronized (wasInterruptedLock) {
wasInterrupted = true;
if (!socket.isClosed()) {
closeSocket();
}
}
throw interruptedException;
}
}
public void connectAsync(final SocketAddress socketAddress, final int timeout) {
AbstractXMPPConnection.asyncGo(new Runnable() {
@Override
public void run() {
try {
socket.connect(socketAddress, timeout);
}
catch (IOException e) {
setException(e);
return;
}
synchronized (wasInterruptedLock) {
if (wasInterrupted) {
closeSocket();
return;
}
}
setResult(socket);
}
});
}
private void closeSocket() {
try {
socket.close();
}
catch (IOException ioException) {
LOGGER.log(Level.WARNING, "Could not close socket", ioException);
}
}
}
public abstract static class InternalProcessStanzaSmackFuture<V, E extends Exception> extends InternalSmackFuture<V, E> public abstract static class InternalProcessStanzaSmackFuture<V, E extends Exception> extends InternalSmackFuture<V, E>
implements StanzaListener, ExceptionCallback<E> { implements StanzaListener, ExceptionCallback<E> {

View file

@ -262,7 +262,7 @@ public class StanzaCollector {
if (!connection.isConnected()) { if (!connection.isConnected()) {
throw new NotConnectedException(connection, packetFilter); throw new NotConnectedException(connection, packetFilter);
} }
throw NoResponseException.newWith(connection, this); throw NoResponseException.newWith(timeout, this);
} }
XMPPErrorException.ifHasErrorThenThrow(result); XMPPErrorException.ifHasErrorThenThrow(result);

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright © 2014-2015 Florian Schmaus * Copyright © 2014-2019 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import java.util.concurrent.locks.Lock;
import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.SmackWrappedException;
import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.packet.Nonza;
import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.TopLevelStreamElement; import org.jivesoftware.smack.packet.TopLevelStreamElement;
@ -37,6 +38,7 @@ public class SynchronizationPoint<E extends Exception> {
// same memory synchronization effects as synchronization block enter and leave. // same memory synchronization effects as synchronization block enter and leave.
private State state; private State state;
private E failureException; private E failureException;
private SmackWrappedException smackWrappedExcpetion;
/** /**
* Construct a new synchronization point for the given connection. * Construct a new synchronization point for the given connection.
@ -59,6 +61,7 @@ public class SynchronizationPoint<E extends Exception> {
connectionLock.lock(); connectionLock.lock();
state = State.Initial; state = State.Initial;
failureException = null; failureException = null;
smackWrappedExcpetion = null;
connectionLock.unlock(); connectionLock.unlock();
} }
@ -71,7 +74,7 @@ public class SynchronizationPoint<E extends Exception> {
* @throws InterruptedException if the connection is interrupted. * @throws InterruptedException if the connection is interrupted.
* @return <code>null</code> if synchronization point was successful, or the failure Exception. * @return <code>null</code> if synchronization point was successful, or the failure Exception.
*/ */
public E sendAndWaitForResponse(TopLevelStreamElement request) throws NoResponseException, public Exception sendAndWaitForResponse(TopLevelStreamElement request) throws NoResponseException,
NotConnectedException, InterruptedException { NotConnectedException, InterruptedException {
assert (state == State.Initial); assert (state == State.Initial);
connectionLock.lock(); connectionLock.lock();
@ -103,15 +106,14 @@ public class SynchronizationPoint<E extends Exception> {
* @throws NoResponseException if no response was received. * @throws NoResponseException if no response was received.
* @throws NotConnectedException if the connection is not connected. * @throws NotConnectedException if the connection is not connected.
* @throws InterruptedException if the connection is interrupted. * @throws InterruptedException if the connection is interrupted.
* @throws SmackWrappedException in case of a wrapped exception;
*/ */
public void sendAndWaitForResponseOrThrow(Nonza request) throws E, NoResponseException, public void sendAndWaitForResponseOrThrow(Nonza request) throws E, NoResponseException,
NotConnectedException, InterruptedException { NotConnectedException, InterruptedException, SmackWrappedException {
sendAndWaitForResponse(request); sendAndWaitForResponse(request);
switch (state) { switch (state) {
case Failure: case Failure:
if (failureException != null) { throwException();
throw failureException;
}
break; break;
default: default:
// Success, do nothing // Success, do nothing
@ -123,11 +125,12 @@ public class SynchronizationPoint<E extends Exception> {
* @throws NoResponseException if there was no response marking the synchronization point as success or failed. * @throws NoResponseException if there was no response marking the synchronization point as success or failed.
* @throws E if there was a failure * @throws E if there was a failure
* @throws InterruptedException if the connection is interrupted. * @throws InterruptedException if the connection is interrupted.
* @throws SmackWrappedException in case of a wrapped exception;
*/ */
public void checkIfSuccessOrWaitOrThrow() throws NoResponseException, E, InterruptedException { public void checkIfSuccessOrWaitOrThrow() throws NoResponseException, E, InterruptedException, SmackWrappedException {
checkIfSuccessOrWait(); checkIfSuccessOrWait();
if (state == State.Failure) { if (state == State.Failure) {
throw failureException; throwException();
} }
} }
@ -137,7 +140,7 @@ public class SynchronizationPoint<E extends Exception> {
* @throws InterruptedException * @throws InterruptedException
* @return <code>null</code> if synchronization point was successful, or the failure Exception. * @return <code>null</code> if synchronization point was successful, or the failure Exception.
*/ */
public E checkIfSuccessOrWait() throws NoResponseException, InterruptedException { public Exception checkIfSuccessOrWait() throws NoResponseException, InterruptedException {
connectionLock.lock(); connectionLock.lock();
try { try {
switch (state) { switch (state) {
@ -145,7 +148,7 @@ public class SynchronizationPoint<E extends Exception> {
case Success: case Success:
return null; return null;
case Failure: case Failure:
return failureException; return getException();
default: default:
// Do nothing // Do nothing
break; break;
@ -198,6 +201,24 @@ public class SynchronizationPoint<E extends Exception> {
} }
} }
/**
* Report this synchronization point as failed because of the given exception. The {@code failureException} must be set.
*
* @param exception the exception causing this synchronization point to fail.
*/
public void reportGenericFailure(SmackWrappedException exception) {
assert exception != null;
connectionLock.lock();
try {
state = State.Failure;
this.smackWrappedExcpetion = exception;
condition.signalAll();
}
finally {
connectionLock.unlock();
}
}
/** /**
* Check if this synchronization point was successful. * Check if this synchronization point was successful.
* *
@ -213,6 +234,16 @@ public class SynchronizationPoint<E extends Exception> {
} }
} }
public boolean isNotInInitialState() {
connectionLock.lock();
try {
return state != State.Initial;
}
finally {
connectionLock.unlock();
}
}
/** /**
* Check if this synchronization point has its request already sent. * Check if this synchronization point has its request already sent.
* *
@ -256,6 +287,20 @@ public class SynchronizationPoint<E extends Exception> {
} }
} }
private Exception getException() {
if (failureException != null) {
return failureException;
}
return smackWrappedExcpetion;
}
private void throwException() throws E, SmackWrappedException {
if (failureException != null) {
throw failureException;
}
throw smackWrappedExcpetion;
}
/** /**
* Check for a response and throw a {@link NoResponseException} if there was none. * Check for a response and throw a {@link NoResponseException} if there was none.
* <p> * <p>
@ -264,7 +309,7 @@ public class SynchronizationPoint<E extends Exception> {
* @return <code>true</code> if synchronization point was successful, <code>false</code> on failure. * @return <code>true</code> if synchronization point was successful, <code>false</code> on failure.
* @throws NoResponseException * @throws NoResponseException
*/ */
private E checkForResponse() throws NoResponseException { private Exception checkForResponse() throws NoResponseException {
switch (state) { switch (state) {
case Initial: case Initial:
case NoResponse: case NoResponse:
@ -273,7 +318,7 @@ public class SynchronizationPoint<E extends Exception> {
case Success: case Success:
return null; return null;
case Failure: case Failure:
return failureException; return getException();
default: default:
throw new AssertionError("Unknown state " + state); throw new AssertionError("Unknown state " + state);
} }

View file

@ -198,6 +198,12 @@ public class StanzaError extends AbstractError implements ExtensionElement {
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder("XMPPError: "); StringBuilder sb = new StringBuilder("XMPPError: ");
sb.append(condition.toString()).append(" - ").append(type.toString()); sb.append(condition.toString()).append(" - ").append(type.toString());
String descriptiveText = getDescriptiveText();
if (descriptiveText != null) {
sb.append(" [").append(descriptiveText).append(']');
}
if (errorGenerator != null) { if (errorGenerator != null) {
sb.append(". Generated by ").append(errorGenerator); sb.append(". Generated by ").append(errorGenerator);
} }
@ -385,11 +391,6 @@ public class StanzaError extends AbstractError implements ExtensionElement {
} }
public static Condition fromString(String string) { public static Condition fromString(String string) {
// Backwards compatibility for older implementations still using RFC 3920. RFC 6120
// changed 'xml-not-well-formed' to 'not-well-formed'.
if ("xml-not-well-formed".equals(string)) {
string = "not-well-formed";
}
string = string.replace('-', '_'); string = string.replace('-', '_');
Condition condition = null; Condition condition = null;
try { try {

View file

@ -186,6 +186,11 @@ public class StreamError extends AbstractError implements Nonza {
} }
public static Condition fromString(String string) { public static Condition fromString(String string) {
// Backwards compatibility for older implementations still using RFC 3920. RFC 6120
// changed 'xml-not-well-formed' to 'not-well-formed'.
if ("xml-not-well-formed".equals(string)) {
string = "not-well-formed";
}
string = string.replace('-', '_'); string = string.replace('-', '_');
Condition condition = null; Condition condition = null;
try { try {

View file

@ -123,7 +123,9 @@ public class IntrospectionProvider{
case "java.lang.String": case "java.lang.String":
return value; return value;
case "boolean": case "boolean":
// CHECKSTYLE:OFF
return Boolean.valueOf(value); return Boolean.valueOf(value);
// CHECKSTYLE:ON
case "int": case "int":
return Integer.valueOf(value); return Integer.valueOf(value);
case "long": case "long":

View file

@ -58,7 +58,7 @@ class HTTPProxySocketConnection implements ProxySocketConnection {
proxyLine = "\r\nProxy-Authorization: Basic " + Base64.encode(username + ":" + password); proxyLine = "\r\nProxy-Authorization: Basic " + Base64.encode(username + ":" + password);
} }
socket.getOutputStream().write((hostport + " HTTP/1.1\r\nHost: " socket.getOutputStream().write((hostport + " HTTP/1.1\r\nHost: "
+ hostport + proxyLine + "\r\n\r\n").getBytes("UTF-8")); + host + ":" + port + proxyLine + "\r\n\r\n").getBytes("UTF-8"));
InputStream in = socket.getInputStream(); InputStream in = socket.getInputStream();
StringBuilder got = new StringBuilder(100); StringBuilder got = new StringBuilder(100);
@ -115,7 +115,8 @@ class HTTPProxySocketConnection implements ProxySocketConnection {
int code = Integer.parseInt(m.group(1)); int code = Integer.parseInt(m.group(1));
if (code != HttpURLConnection.HTTP_OK) { if (code != HttpURLConnection.HTTP_OK) {
throw new ProxyException(ProxyInfo.ProxyType.HTTP); throw new ProxyException(ProxyInfo.ProxyType.HTTP,
"Error code in proxy response: " + code);
} }
} }

View file

@ -129,6 +129,27 @@ public class ParserUtils {
return Resourcepart.from(resourcepartString); return Resourcepart.from(resourcepartString);
} }
/**
* Prase a string to a boolean value as per "xs:boolean". Valid input strings are "true", "1" for true, and "false", "0" for false.
*
* @param booleanString the input string.
* @return the boolean representation of the input string
* @throws IllegalArgumentException if the input string is not valid.
* @since 4.3.2
*/
public static boolean parseXmlBoolean(String booleanString) {
switch (booleanString) {
case "true":
case "1":
return true;
case "false":
case "0":
return false;
default:
throw new IllegalArgumentException(booleanString + " is not a valid boolean string");
}
}
/** /**
* Get the boolean value of an argument. * Get the boolean value of an argument.
* *
@ -141,7 +162,7 @@ public class ParserUtils {
if (valueString == null) if (valueString == null)
return null; return null;
valueString = valueString.toLowerCase(Locale.US); valueString = valueString.toLowerCase(Locale.US);
return valueString.equals("true") || valueString.equals("0"); return parseXmlBoolean(valueString);
} }
public static boolean getBooleanAttribute(XmlPullParser parser, String name, public static boolean getBooleanAttribute(XmlPullParser parser, String name,

View file

@ -35,7 +35,11 @@ public class XmlUtil {
private static final TransformerFactory transformerFactory = TransformerFactory.newInstance(); private static final TransformerFactory transformerFactory = TransformerFactory.newInstance();
static { static {
try {
transformerFactory.setAttribute("indent-number", 2); transformerFactory.setAttribute("indent-number", 2);
} catch (IllegalArgumentException e) {
LOGGER.log(Level.INFO, "XML TransformerFactory does not support indent-number attribute", e);
}
} }
public static String prettyFormatXml(CharSequence xml) { public static String prettyFormatXml(CharSequence xml) {

View file

@ -0,0 +1,79 @@
/**
*
* Copyright 2018 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.jxmpp.jid.JidTestUtil;
public class ConnectionConfigurationTest {
@Test
public void setIp() {
DummyConnectionConfiguration.Builder builder = newUnitTestBuilder();
final String ip = "192.168.0.1";
builder.setHostAddressByNameOrIp(ip);
DummyConnectionConfiguration connectionConfiguration = builder.build();
assertEquals('/' + ip, connectionConfiguration.getHostAddress().toString());
}
@Test
public void setFqdn() {
DummyConnectionConfiguration.Builder builder = newUnitTestBuilder();
final String fqdn = "foo.example.org";
builder.setHostAddressByNameOrIp(fqdn);
DummyConnectionConfiguration connectionConfiguration = builder.build();
assertEquals(fqdn, connectionConfiguration.getHost().toString());
}
private static DummyConnectionConfiguration.Builder newUnitTestBuilder() {
DummyConnectionConfiguration.Builder builder = DummyConnectionConfiguration.builder();
builder.setXmppDomain(JidTestUtil.DOMAIN_BARE_JID_1);
return builder;
}
private static final class DummyConnectionConfiguration extends ConnectionConfiguration {
protected DummyConnectionConfiguration(Builder builder) {
super(builder);
}
public static Builder builder() {
return new Builder();
}
private static final class Builder
extends ConnectionConfiguration.Builder<Builder, DummyConnectionConfiguration> {
@Override
public DummyConnectionConfiguration build() {
return new DummyConnectionConfiguration(this);
}
@Override
protected Builder getThis() {
return this;
}
}
}
}

View file

@ -100,6 +100,11 @@ public class DummyConnection extends AbstractXMPPConnection {
callConnectionClosedListener(); callConnectionClosedListener();
} }
@Override
public void instantShutdown() {
shutdown();
}
@Override @Override
public boolean isSecureConnection() { public boolean isSecureConnection() {
return false; return false;
@ -226,4 +231,5 @@ public class DummyConnection extends AbstractXMPPConnection {
} }
} }
} }
} }

View file

@ -104,4 +104,22 @@ public class StreamErrorTest {
assertNotNull(appSpecificElement); assertNotNull(appSpecificElement);
} }
@Test
public void testStreamErrorXmlNotWellFormed() {
StreamError error = null;
final String xml =
// Usually the stream:stream element has more attributes (to, version, ...)
// We omit those, since they are not relevant for testing
"<stream:stream from='im.example.com' id='++TR84Sm6A3hnt3Q065SnAbbk3Y=' xmlns:stream='http://etherx.jabber.org/streams'>" +
"<stream:error><xml-not-well-formed xmlns='urn:ietf:params:xml:ns:xmpp-streams'/></stream:error>" +
"</stream:stream>";
try {
XmlPullParser parser = PacketParserUtils.getParserFor(xml, "error");
error = PacketParserUtils.parseStreamError(parser);
} catch (Exception e) {
fail(e.getMessage());
}
assertNotNull(error);
assertEquals(Condition.not_well_formed, error.getCondition());
}
} }

View file

@ -16,6 +16,8 @@
*/ */
package org.jivesoftware.smackx.hoxt.provider; package org.jivesoftware.smackx.hoxt.provider;
import org.jivesoftware.smack.util.ParserUtils;
import org.jivesoftware.smackx.hoxt.packet.HttpMethod; import org.jivesoftware.smackx.hoxt.packet.HttpMethod;
import org.jivesoftware.smackx.hoxt.packet.HttpOverXmppReq; import org.jivesoftware.smackx.hoxt.packet.HttpOverXmppReq;
@ -47,13 +49,13 @@ public class HttpOverXmppReqProvider extends AbstractHttpOverXmppProvider<HttpOv
String jingleStr = parser.getAttributeValue("", AbstractHttpOverXmppProvider.ELEMENT_JINGLE); String jingleStr = parser.getAttributeValue("", AbstractHttpOverXmppProvider.ELEMENT_JINGLE);
if (sipubStr != null) { if (sipubStr != null) {
builder.setSipub(Boolean.valueOf(sipubStr)); builder.setSipub(ParserUtils.parseXmlBoolean(sipubStr));
} }
if (ibbStr != null) { if (ibbStr != null) {
builder.setIbb(Boolean.valueOf(ibbStr)); builder.setIbb(ParserUtils.parseXmlBoolean(ibbStr));
} }
if (jingleStr != null) { if (jingleStr != null) {
builder.setJingle(Boolean.valueOf(jingleStr)); builder.setJingle(ParserUtils.parseXmlBoolean(jingleStr));
} }
String maxChunkSize = parser.getAttributeValue("", ATTRIBUTE_MAX_CHUNK_SIZE); String maxChunkSize = parser.getAttributeValue("", ATTRIBUTE_MAX_CHUNK_SIZE);

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright © 2016 Florian Schmaus * Copyright © 2016-2018 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,8 @@
*/ */
package org.jivesoftware.smackx.iot.control.element; package org.jivesoftware.smackx.iot.control.element;
import org.jivesoftware.smack.util.ParserUtils;
public class SetBoolData extends SetData { public class SetBoolData extends SetData {
public SetBoolData(String name, boolean value) { public SetBoolData(String name, boolean value) {
@ -31,7 +33,7 @@ public class SetBoolData extends SetData {
public Boolean getBooleanValue() { public Boolean getBooleanValue() {
if (booleanCache != null) { if (booleanCache != null) {
booleanCache = Boolean.valueOf(getValue()); booleanCache = ParserUtils.parseXmlBoolean(getValue());
} }
return booleanCache; return booleanCache;
} }

View file

@ -32,7 +32,7 @@ public final class JingleFileTransferManager extends Manager {
super(connection); super(connection);
} }
public static JingleFileTransferManager getInstanceFor(XMPPConnection connection) { public static synchronized JingleFileTransferManager getInstanceFor(XMPPConnection connection) {
JingleFileTransferManager manager = INSTANCES.get(connection); JingleFileTransferManager manager = INSTANCES.get(connection);
if (manager == null) { if (manager == null) {
manager = new JingleFileTransferManager(connection); manager = new JingleFileTransferManager(connection);

View file

@ -114,8 +114,8 @@ import org.jxmpp.jid.Jid;
* <pre> * <pre>
* {@code * {@code
* MamQueryArgs mamQueryArgs = MamQueryArgs.builder() * MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
* .withJid(jid) * .limitResultsToJid(jid)
* .setResultPageSize(10) * .setResultPageSizeTo(10)
* .queryLastPage() * .queryLastPage()
* .build(); * .build();
* MamQuery mamQuery = mamManager.queryArchive(mamQueryArgs); * MamQuery mamQuery = mamManager.queryArchive(mamQueryArgs);
@ -178,7 +178,9 @@ public final class MamManager extends Manager {
* @param connection the XMPP connection to get the archive for. * @param connection the XMPP connection to get the archive for.
* @return the instance of MamManager. * @return the instance of MamManager.
*/ */
// CHECKSTYLE:OFF:RegexpSingleline
public static MamManager getInstanceFor(XMPPConnection connection) { public static MamManager getInstanceFor(XMPPConnection connection) {
// CHECKSTYLE:ON:RegexpSingleline
return getInstanceFor(connection, (Jid) null); return getInstanceFor(connection, (Jid) null);
} }
@ -995,7 +997,7 @@ public final class MamManager extends Manager {
public List<Message> pagePrevious(int count) throws NoResponseException, XMPPErrorException, public List<Message> pagePrevious(int count) throws NoResponseException, XMPPErrorException,
NotConnectedException, NotLoggedInException, InterruptedException { NotConnectedException, NotLoggedInException, InterruptedException {
RSMSet previousResultRsmSet = getPreviousRsmSet(); RSMSet previousResultRsmSet = getPreviousRsmSet();
RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.before); RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before);
return page(requestRsmSet); return page(requestRsmSet);
} }

View file

@ -51,7 +51,7 @@ public final class ReferenceManager extends Manager {
* @param connection xmpp connection * @param connection xmpp connection
* @return reference manager instance * @return reference manager instance
*/ */
public static ReferenceManager getInstanceFor(XMPPConnection connection) { public static synchronized ReferenceManager getInstanceFor(XMPPConnection connection) {
ReferenceManager manager = INSTANCES.get(connection); ReferenceManager manager = INSTANCES.get(connection);
if (manager == null) { if (manager == null) {
manager = new ReferenceManager(connection); manager = new ReferenceManager(connection);

View file

@ -27,7 +27,6 @@ import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.MessageTypeFilter; import org.jivesoftware.smack.filter.MessageTypeFilter;
import org.jivesoftware.smack.filter.NotFilter; import org.jivesoftware.smack.filter.NotFilter;
import org.jivesoftware.smack.filter.StanzaExtensionFilter;
import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.filter.ToTypeFilter; import org.jivesoftware.smack.filter.ToTypeFilter;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
@ -46,13 +45,12 @@ public final class StableUniqueStanzaIdManager extends Manager {
MessageTypeFilter.NORMAL_OR_CHAT_OR_HEADLINE, MessageTypeFilter.NORMAL_OR_CHAT_OR_HEADLINE,
ToTypeFilter.ENTITY_FULL_OR_BARE_JID); ToTypeFilter.ENTITY_FULL_OR_BARE_JID);
private static final StanzaFilter ORIGIN_ID_FILTER = new StanzaExtensionFilter(OriginIdElement.ELEMENT, NAMESPACE);
// Listener for outgoing stanzas that adds origin-ids to outgoing stanzas. // Listener for outgoing stanzas that adds origin-ids to outgoing stanzas.
private final StanzaListener stanzaListener = new StanzaListener() { private static final StanzaListener ADD_ORIGIN_ID_INTERCEPTOR = new StanzaListener() {
@Override @Override
public void processStanza(Stanza stanza) { public void processStanza(Stanza stanza) {
OriginIdElement.addOriginId((Message) stanza); Message message = (Message) stanza;
OriginIdElement.addOriginId(message);
} }
}; };
@ -80,7 +78,7 @@ public final class StableUniqueStanzaIdManager extends Manager {
* @param connection xmpp-connection * @param connection xmpp-connection
* @return manager instance for the connection * @return manager instance for the connection
*/ */
public static StableUniqueStanzaIdManager getInstanceFor(XMPPConnection connection) { public static synchronized StableUniqueStanzaIdManager getInstanceFor(XMPPConnection connection) {
StableUniqueStanzaIdManager manager = INSTANCES.get(connection); StableUniqueStanzaIdManager manager = INSTANCES.get(connection);
if (manager == null) { if (manager == null) {
manager = new StableUniqueStanzaIdManager(connection); manager = new StableUniqueStanzaIdManager(connection);
@ -95,7 +93,7 @@ public final class StableUniqueStanzaIdManager extends Manager {
public synchronized void enable() { public synchronized void enable() {
ServiceDiscoveryManager.getInstanceFor(connection()).addFeature(NAMESPACE); ServiceDiscoveryManager.getInstanceFor(connection()).addFeature(NAMESPACE);
StanzaFilter filter = new AndFilter(OUTGOING_FILTER, new NotFilter(OUTGOING_FILTER)); StanzaFilter filter = new AndFilter(OUTGOING_FILTER, new NotFilter(OUTGOING_FILTER));
connection().addStanzaInterceptor(stanzaListener, filter); connection().addStanzaInterceptor(ADD_ORIGIN_ID_INTERCEPTOR, filter);
} }
/** /**
@ -103,7 +101,7 @@ public final class StableUniqueStanzaIdManager extends Manager {
*/ */
public synchronized void disable() { public synchronized void disable() {
ServiceDiscoveryManager.getInstanceFor(connection()).removeFeature(NAMESPACE); ServiceDiscoveryManager.getInstanceFor(connection()).removeFeature(NAMESPACE);
connection().removeStanzaInterceptor(stanzaListener); connection().removeStanzaInterceptor(ADD_ORIGIN_ID_INTERCEPTOR);
} }
/** /**

View file

@ -23,7 +23,11 @@ import org.xmlpull.v1.XmlPullParser;
public class OriginIdProvider extends ExtensionElementProvider<OriginIdElement> { public class OriginIdProvider extends ExtensionElementProvider<OriginIdElement> {
public static final OriginIdProvider TEST_INSTANCE = new OriginIdProvider(); public static final OriginIdProvider INSTANCE = new OriginIdProvider();
// TODO: Remove in Smack 4.4.
@Deprecated
public static final OriginIdProvider TEST_INSTANCE = INSTANCE;
@Override @Override
public OriginIdElement parse(XmlPullParser parser, int initialDepth) throws Exception { public OriginIdElement parse(XmlPullParser parser, int initialDepth) throws Exception {

View file

@ -23,7 +23,11 @@ import org.xmlpull.v1.XmlPullParser;
public class StanzaIdProvider extends ExtensionElementProvider<StanzaIdElement> { public class StanzaIdProvider extends ExtensionElementProvider<StanzaIdElement> {
public static StanzaIdProvider TEST_INSTANCE = new StanzaIdProvider(); public static final StanzaIdProvider INSTANCE = new StanzaIdProvider();
// TODO: Remove in Smack 4.4.
@Deprecated
public static final StanzaIdProvider TEST_INSTANCE = INSTANCE;
@Override @Override
public StanzaIdElement parse(XmlPullParser parser, int initialDepth) throws Exception { public StanzaIdElement parse(XmlPullParser parser, int initialDepth) throws Exception {

View file

@ -61,7 +61,7 @@ public final class SpoilerManager extends Manager {
* @param connection xmpp connection * @param connection xmpp connection
* @return SpoilerManager * @return SpoilerManager
*/ */
public static SpoilerManager getInstanceFor(XMPPConnection connection) { public static synchronized SpoilerManager getInstanceFor(XMPPConnection connection) {
SpoilerManager manager = INSTANCES.get(connection); SpoilerManager manager = INSTANCES.get(connection);
if (manager == null) { if (manager == null) {
manager = new SpoilerManager(connection); manager = new SpoilerManager(connection);

View file

@ -25,6 +25,7 @@ import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.test.util.SmackTestSuite; import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smack.test.util.TestUtils; import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smackx.sid.element.OriginIdElement; import org.jivesoftware.smackx.sid.element.OriginIdElement;
import org.jivesoftware.smackx.sid.element.StanzaIdElement; import org.jivesoftware.smackx.sid.element.StanzaIdElement;
import org.jivesoftware.smackx.sid.provider.OriginIdProvider; import org.jivesoftware.smackx.sid.provider.OriginIdProvider;
@ -42,7 +43,7 @@ public class StableUniqueStanzaIdTest extends SmackTestSuite {
assertEquals("alice@wonderland.lit", element.getBy()); assertEquals("alice@wonderland.lit", element.getBy());
assertXMLEqual(xml, element.toXML(null).toString()); assertXMLEqual(xml, element.toXML(null).toString());
StanzaIdElement parsed = StanzaIdProvider.TEST_INSTANCE.parse(TestUtils.getParser(xml)); StanzaIdElement parsed = StanzaIdProvider.INSTANCE.parse(TestUtils.getParser(xml));
assertEquals(element.getId(), parsed.getId()); assertEquals(element.getId(), parsed.getId());
assertEquals(element.getBy(), parsed.getBy()); assertEquals(element.getBy(), parsed.getBy());
} }
@ -54,7 +55,7 @@ public class StableUniqueStanzaIdTest extends SmackTestSuite {
assertEquals("de305d54-75b4-431b-adb2-eb6b9e546013", element.getId()); assertEquals("de305d54-75b4-431b-adb2-eb6b9e546013", element.getId());
assertXMLEqual(xml, element.toXML(null).toString()); assertXMLEqual(xml, element.toXML(null).toString());
OriginIdElement parsed = OriginIdProvider.TEST_INSTANCE.parse(TestUtils.getParser(xml)); OriginIdElement parsed = OriginIdProvider.INSTANCE.parse(TestUtils.getParser(xml));
assertEquals(element.getId(), parsed.getId()); assertEquals(element.getId(), parsed.getId());
} }
@ -81,4 +82,17 @@ public class StableUniqueStanzaIdTest extends SmackTestSuite {
assertTrue(StanzaIdElement.hasStanzaId(message)); assertTrue(StanzaIdElement.hasStanzaId(message));
assertEquals(stanzaId, StanzaIdElement.getStanzaId(message)); assertEquals(stanzaId, StanzaIdElement.getStanzaId(message));
} }
@Test
public void testMultipleUssidExtensions() throws Exception {
String message = "<message xmlns='jabber:client' from='e4aec989-3e20-4846-83bf-f50df89b5d07@muclight.example.com/user1@example.com' to='user1@example.com' id='6b71fe3a-3cb2-489c-9c8e-b6879761d15e' type='groupchat'>" +
"<body>Test message</body>" +
"<markable xmlns='urn:xmpp:chat-markers:0'/>" +
"<stanza-id by='e4aec989-3e20-4846-83bf-f50df89b5d07@muclight.example.com' id='B0KK24ETVC81' xmlns='urn:xmpp:sid:0'/>" +
"<stanza-id by='user1@example.com' id='B0KK24EV89G1' xmlns='urn:xmpp:sid:0'/>" +
"</message>";
Message messageStanza = PacketParserUtils.parseStanza(message);
assertTrue(StanzaIdElement.hasStanzaId(messageStanza));
}
} }

View file

@ -269,12 +269,12 @@ public class Bookmarks implements PrivateData {
private static BookmarkedConference getConferenceStorage(XmlPullParser parser) throws XmlPullParserException, IOException { private static BookmarkedConference getConferenceStorage(XmlPullParser parser) throws XmlPullParserException, IOException {
String name = parser.getAttributeValue("", "name"); String name = parser.getAttributeValue("", "name");
String autojoin = parser.getAttributeValue("", "autojoin"); boolean autojoin = ParserUtils.getBooleanAttribute(parser, "autojoin", false);
EntityBareJid jid = ParserUtils.getBareJidAttribute(parser); EntityBareJid jid = ParserUtils.getBareJidAttribute(parser);
BookmarkedConference conf = new BookmarkedConference(jid); BookmarkedConference conf = new BookmarkedConference(jid);
conf.setName(name); conf.setName(name);
conf.setAutoJoin(Boolean.valueOf(autojoin)); conf.setAutoJoin(autojoin);
// Check for nickname // Check for nickname
boolean done = false; boolean done = false;

View file

@ -211,6 +211,7 @@ public final class Socks5Proxy {
if (this.serverSocket != null) { if (this.serverSocket != null) {
this.serverThread = new Thread(this.serverProcess); this.serverThread = new Thread(this.serverProcess);
this.serverThread.setName("Smack Local SOCKS5 Proxy"); this.serverThread.setName("Smack Local SOCKS5 Proxy");
this.serverThread.setDaemon(true);
this.serverThread.start(); this.serverThread.start();
} }
} }

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2003-2007 Jive Software, 2018 Florian Schmaus. * Copyright 2003-2007 Jive Software, 2018-2019 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -227,7 +227,7 @@ public final class ServiceDiscoveryManager extends Manager {
/** /**
* Returns the type of client that will be returned when asked for the client identity in a * Returns the type of client that will be returned when asked for the client identity in a
* disco request. The valid types are defined by the category client. Follow this link to learn * disco request. The valid types are defined by the category client. Follow this link to learn
* the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>. * the possible types: <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>
* *
* @return the type of client that will be returned when asked for the client identity in a * @return the type of client that will be returned when asked for the client identity in a
* disco request. * disco request.
@ -271,8 +271,8 @@ public final class ServiceDiscoveryManager extends Manager {
*/ */
public Set<DiscoverInfo.Identity> getIdentities() { public Set<DiscoverInfo.Identity> getIdentities() {
Set<Identity> res = new HashSet<>(identities); Set<Identity> res = new HashSet<>(identities);
// Add the default identity that must exist // Add the main identity that must exist
res.add(defaultIdentity); res.add(identity);
return Collections.unmodifiableSet(res); return Collections.unmodifiableSet(res);
} }

View file

@ -263,7 +263,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable<DiscoverInfo> {
* Represents the identity of a given XMPP entity. An entity may have many identities but all * Represents the identity of a given XMPP entity. An entity may have many identities but all
* the identities SHOULD have the same name.<p> * the identities SHOULD have the same name.<p>
* *
* Refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> * Refer to <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>
* in order to get the official registry of values for the <i>category</i> and <i>type</i> * in order to get the official registry of values for the <i>category</i> and <i>type</i>
* attributes. * attributes.
* *
@ -327,7 +327,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable<DiscoverInfo> {
/** /**
* Returns the entity's category. To get the official registry of values for the * Returns the entity's category. To get the official registry of values for the
* 'category' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> * 'category' attribute refer to <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>.
* *
* @return the entity's category. * @return the entity's category.
*/ */
@ -346,7 +346,7 @@ public class DiscoverInfo extends IQ implements TypedCloneable<DiscoverInfo> {
/** /**
* Returns the entity's type. To get the official registry of values for the * Returns the entity's type. To get the official registry of values for the
* 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> * 'type' attribute refer to <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>.
* *
* @return the entity's type. * @return the entity's type.
*/ */

View file

@ -48,7 +48,7 @@ public final class JingleTransportMethodManager extends Manager {
super(connection); super(connection);
} }
public static JingleTransportMethodManager getInstanceFor(XMPPConnection connection) { public static synchronized JingleTransportMethodManager getInstanceFor(XMPPConnection connection) {
JingleTransportMethodManager manager = INSTANCES.get(connection); JingleTransportMethodManager manager = INSTANCES.get(connection);
if (manager == null) { if (manager == null) {
manager = new JingleTransportMethodManager(connection); manager = new JingleTransportMethodManager(connection);

View file

@ -38,7 +38,7 @@ public final class JingleIBBTransportManager extends JingleTransportManager<Jing
JingleContentProviderManager.addJingleContentTransportProvider(getNamespace(), new JingleIBBTransportProvider()); JingleContentProviderManager.addJingleContentTransportProvider(getNamespace(), new JingleIBBTransportProvider());
} }
public static JingleIBBTransportManager getInstanceFor(XMPPConnection connection) { public static synchronized JingleIBBTransportManager getInstanceFor(XMPPConnection connection) {
JingleIBBTransportManager manager = INSTANCES.get(connection); JingleIBBTransportManager manager = INSTANCES.get(connection);
if (manager == null) { if (manager == null) {
manager = new JingleIBBTransportManager(connection); manager = new JingleIBBTransportManager(connection);

View file

@ -63,7 +63,7 @@ public final class JingleS5BTransportManager extends JingleTransportManager<Jing
JingleContentProviderManager.addJingleContentTransportProvider(getNamespace(), new JingleS5BTransportProvider()); JingleContentProviderManager.addJingleContentTransportProvider(getNamespace(), new JingleS5BTransportProvider());
} }
public static JingleS5BTransportManager getInstanceFor(XMPPConnection connection) { public static synchronized JingleS5BTransportManager getInstanceFor(XMPPConnection connection) {
JingleS5BTransportManager manager = INSTANCES.get(connection); JingleS5BTransportManager manager = INSTANCES.get(connection);
if (manager == null) { if (manager == null) {
manager = new JingleS5BTransportManager(connection); manager = new JingleS5BTransportManager(connection);

View file

@ -92,7 +92,9 @@ public class JivePropertiesExtensionProvider extends ExtensionElementProvider<Ji
value = Double.valueOf(valueText); value = Double.valueOf(valueText);
} }
else if ("boolean".equals(type)) { else if ("boolean".equals(type)) {
// CHECKSTYLE:OFF
value = Boolean.valueOf(valueText); value = Boolean.valueOf(valueText);
// CHECKSTYLE:ON
} }
else if ("string".equals(type)) { else if ("string".equals(type)) {
value = valueText; value = valueText;

View file

@ -205,7 +205,6 @@ public class MultiUserChat {
final Presence presence = (Presence) packet; final Presence presence = (Presence) packet;
final EntityFullJid from = presence.getFrom().asEntityFullJidIfPossible(); final EntityFullJid from = presence.getFrom().asEntityFullJidIfPossible();
if (from == null) { if (from == null) {
LOGGER.warning("Presence not from a full JID: " + presence.getFrom());
return; return;
} }
final EntityFullJid myRoomJID = myRoomJid; final EntityFullJid myRoomJID = myRoomJid;
@ -341,8 +340,9 @@ public class MultiUserChat {
// Setup the messageListeners and presenceListeners *before* the join presence is send. // Setup the messageListeners and presenceListeners *before* the join presence is send.
connection.addSyncStanzaListener(messageListener, fromRoomGroupchatFilter); connection.addSyncStanzaListener(messageListener, fromRoomGroupchatFilter);
connection.addSyncStanzaListener(presenceListener, new AndFilter(fromRoomFilter, StanzaFilter presenceFromRoomFilter = new AndFilter(fromRoomFilter,
StanzaTypeFilter.PRESENCE)); StanzaTypeFilter.PRESENCE);
connection.addSyncStanzaListener(presenceListener, presenceFromRoomFilter);
// @formatter:off // @formatter:off
connection.addSyncStanzaListener(subjectListener, connection.addSyncStanzaListener(subjectListener,
new AndFilter(fromRoomFilter, new AndFilter(fromRoomFilter,
@ -371,15 +371,27 @@ public class MultiUserChat {
) )
); );
// @formatter:on // @formatter:on
StanzaCollector presenceStanzaCollector = null;
Presence presence; Presence presence;
try { try {
presence = connection.createStanzaCollectorAndSend(responseFilter, joinPresence).nextResultOrThrow(conf.getTimeout()); // This stanza collector will collect the final self presence from the MUC, which also signals that we have successful entered the MUC.
StanzaCollector selfPresenceCollector = connection.createStanzaCollectorAndSend(responseFilter, joinPresence);
StanzaCollector.Configuration presenceStanzaCollectorConfguration = StanzaCollector.newConfiguration().setCollectorToReset(
selfPresenceCollector).setStanzaFilter(presenceFromRoomFilter);
// This stanza collector is used to reset the timeout of the selfPresenceCollector.
presenceStanzaCollector = connection.createStanzaCollector(presenceStanzaCollectorConfguration);
presence = selfPresenceCollector.nextResultOrThrow(conf.getTimeout());
} }
catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) { catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) {
// Ensure that all callbacks are removed if there is an exception // Ensure that all callbacks are removed if there is an exception
removeConnectionCallbacks(); removeConnectionCallbacks();
throw e; throw e;
} }
finally {
if (presenceStanzaCollector != null) {
presenceStanzaCollector.cancel();
}
}
// This presence must be send from a full JID. We use the resourcepart of this JID as nick, since the room may // This presence must be send from a full JID. We use the resourcepart of this JID as nick, since the room may
// performed roomnick rewriting // performed roomnick rewriting
@ -735,7 +747,12 @@ public class MultiUserChat {
// If we've already joined the room, leave it before joining under a new // If we've already joined the room, leave it before joining under a new
// nickname. // nickname.
if (joined) { if (joined) {
leave(); try {
leaveSync();
}
catch (XMPPErrorException | NoResponseException | MucNotJoinedException e) {
LOGGER.log(Level.WARNING, "Could not leave MUC prior joining, assuming we are not joined", e);
}
} }
enter(mucEnterConfiguration); enter(mucEnterConfiguration);
} }
@ -776,6 +793,50 @@ public class MultiUserChat {
connection.sendStanza(leavePresence); connection.sendStanza(leavePresence);
} }
/**
* Leave the chat room.
*
* @return the leave presence as reflected by the MUC.
* @throws NotConnectedException
* @throws InterruptedException
* @throws XMPPErrorException
* @throws NoResponseException
* @throws MucNotJoinedException
*/
public synchronized Presence leaveSync()
throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, MucNotJoinedException {
// Note that this method is intentionally not guarded by
// "if (!joined) return" because it should be always be possible to leave the room in case the instance's
// state does not reflect the actual state.
// Reset occupant information first so that we are assume that we left the room even if sendStanza() would
// throw.
userHasLeft();
final EntityFullJid myRoomJid = this.myRoomJid;
if (myRoomJid == null) {
throw new MucNotJoinedException(this);
}
// We leave a room by sending a presence packet where the "to"
// field is in the form "roomName@service/nickname"
Presence leavePresence = new Presence(Presence.Type.unavailable);
leavePresence.setTo(myRoomJid);
StanzaFilter reflectedLeavePresenceFilter = new AndFilter(
StanzaTypeFilter.PRESENCE,
new StanzaIdFilter(leavePresence),
new OrFilter(
new AndFilter(FromMatchesFilter.createFull(myRoomJid), PresenceTypeFilter.UNAVAILABLE, MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF),
new AndFilter(fromRoomFilter, PresenceTypeFilter.ERROR)
)
);
Presence reflectedLeavePresence = connection.createStanzaCollectorAndSend(reflectedLeavePresenceFilter, leavePresence).nextResultOrThrow();
return reflectedLeavePresence;
}
/** /**
* Get a {@link MucConfigFormManager} to configure this room. * Get a {@link MucConfigFormManager} to configure this room.
* <p> * <p>

View file

@ -19,6 +19,7 @@ package org.jivesoftware.smackx.muc;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -392,19 +393,46 @@ public final class MultiUserChatManager extends Manager {
* @throws NotConnectedException * @throws NotConnectedException
* @throws InterruptedException * @throws InterruptedException
* @throws NotAMucServiceException * @throws NotAMucServiceException
* @deprecated use {@link #getRoomsHostedBy(DomainBareJid)} instead.
*/ */
@Deprecated
// TODO: Remove in Smack 4.4.
public List<HostedRoom> getHostedRooms(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException, public List<HostedRoom> getHostedRooms(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException,
NotConnectedException, InterruptedException, NotAMucServiceException { NotConnectedException, InterruptedException, NotAMucServiceException {
Map<EntityBareJid, HostedRoom> hostedRooms = getRoomsHostedBy(serviceName);
return new ArrayList<>(hostedRooms.values());
}
/**
* Returns a Map of HostedRooms where each HostedRoom has the XMPP address of the room and the room's name.
* Once discovered the rooms hosted by a chat service it is possible to discover more detailed room information or
* join the room.
*
* @param serviceName the service that is hosting the rooms to discover.
* @return a map from the room's address to its HostedRoom information.
* @throws XMPPErrorException
* @throws NoResponseException
* @throws NotConnectedException
* @throws InterruptedException
* @throws NotAMucServiceException
* @since 4.3.1
*/
public Map<EntityBareJid, HostedRoom> getRoomsHostedBy(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException,
NotConnectedException, InterruptedException, NotAMucServiceException {
if (!providesMucService(serviceName)) { if (!providesMucService(serviceName)) {
throw new NotAMucServiceException(serviceName); throw new NotAMucServiceException(serviceName);
} }
ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection()); ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection());
DiscoverItems discoverItems = discoManager.discoverItems(serviceName); DiscoverItems discoverItems = discoManager.discoverItems(serviceName);
List<DiscoverItems.Item> items = discoverItems.getItems(); List<DiscoverItems.Item> items = discoverItems.getItems();
List<HostedRoom> answer = new ArrayList<HostedRoom>(items.size());
Map<EntityBareJid, HostedRoom> answer = new HashMap<>(items.size());
for (DiscoverItems.Item item : items) { for (DiscoverItems.Item item : items) {
answer.add(new HostedRoom(item)); HostedRoom hostedRoom = new HostedRoom(item);
HostedRoom previousRoom = answer.put(hostedRoom.getJid(), hostedRoom);
assert previousRoom == null;
} }
return answer; return answer;
} }

View file

@ -207,7 +207,8 @@ public class RoomInfo {
FormField subjectmodField = form.getField("muc#roominfo_subjectmod"); FormField subjectmodField = form.getField("muc#roominfo_subjectmod");
if (subjectmodField != null && !subjectmodField.getValues().isEmpty()) { if (subjectmodField != null && !subjectmodField.getValues().isEmpty()) {
subjectmod = Boolean.valueOf(subjectmodField.getFirstValue()); String firstValue = subjectmodField.getFirstValue();
subjectmod = ("true".equals(firstValue) || "1".equals(firstValue));
} }
FormField urlField = form.getField("muc#roominfo_logs"); FormField urlField = form.getField("muc#roominfo_logs");

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2013-2014 Georg Lukas, 2015 Florian Schmaus * Copyright 2013-2014 Georg Lukas, 2015-2019 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -74,8 +74,16 @@ import org.jxmpp.jid.Jid;
*/ */
public final class DeliveryReceiptManager extends Manager { public final class DeliveryReceiptManager extends Manager {
private static final StanzaFilter MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST = new AndFilter(StanzaTypeFilter.MESSAGE, /**
new StanzaExtensionFilter(new DeliveryReceiptRequest())); * Filters all non-error messages with receipt requests.
* See <a href="https://xmpp.org/extensions/xep-0184.html#when">XEP-0184 § 5.</a> "A sender could request receipts
* on any non-error content message (chat, groupchat, headline, or normal)"
*/
private static final StanzaFilter NON_ERROR_GROUPCHAT_MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST = new AndFilter(
StanzaTypeFilter.MESSAGE,
new StanzaExtensionFilter(new DeliveryReceiptRequest()),
new NotFilter(MessageTypeFilter.ERROR));
private static final StanzaFilter MESSAGES_WITH_DELIVERY_RECEIPT = new AndFilter(StanzaTypeFilter.MESSAGE, private static final StanzaFilter MESSAGES_WITH_DELIVERY_RECEIPT = new AndFilter(StanzaTypeFilter.MESSAGE,
new StanzaExtensionFilter(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE)); new StanzaExtensionFilter(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE));
@ -175,7 +183,7 @@ public final class DeliveryReceiptManager extends Manager {
} }
connection.sendStanza(ack); connection.sendStanza(ack);
} }
}, MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST); }, NON_ERROR_GROUPCHAT_MESSAGES_WITH_DELIVERY_RECEIPT_REQUEST);
} }
/** /**

View file

@ -261,12 +261,14 @@ public class FormField implements NamedElement {
*/ */
public String getFirstValue() { public String getFirstValue() {
CharSequence firstValue; CharSequence firstValue;
synchronized (values) { synchronized (values) {
firstValue = values.get(0); if (values.isEmpty()) {
}
if (firstValue == null) {
return null; return null;
} }
firstValue = values.get(0);
}
return firstValue.toString(); return firstValue.toString();
} }

View file

@ -951,8 +951,7 @@ public final class Roster extends Manager {
// This is used in case no available presence is found // This is used in case no available presence is found
Presence unavailable = null; Presence unavailable = null;
for (Resourcepart resource : userPresences.keySet()) { for (Presence p : userPresences.values()) {
Presence p = userPresences.get(resource);
if (!p.isAvailable()) { if (!p.isAvailable()) {
unavailable = p; unavailable = p;
continue; continue;
@ -1472,7 +1471,29 @@ public final class Roster extends Manager {
final Presence presence = (Presence) packet; final Presence presence = (Presence) packet;
final Jid from = presence.getFrom(); final Jid from = presence.getFrom();
final BareJid key = from != null ? from.asBareJid() : null; final BareJid key;
if (from != null) {
key = from.asBareJid();
} else {
XMPPConnection connection = connection();
if (connection == null) {
LOGGER.finest("Connection was null while trying to handle exotic presence stanza: " + presence);
return;
}
// Assume the presence come "from the users account on the server" since no from was set (RFC 6120 §
// 8.1.2.1 4.). Note that getUser() may return null, but should never return null in this case as where
// connected.
EntityFullJid myJid = connection.getUser();
if (myJid == null) {
LOGGER.info(
"Connection had no local address in Roster's presence listener."
+ " Possibly we received a presence without from before being authenticated."
+ " Presence: " + presence);
return;
}
LOGGER.info("Exotic presence stanza without from received: " + presence);
key = myJid.asBareJid();
}
asyncButOrdered.performAsyncButOrdered(key, new Runnable() { asyncButOrdered.performAsyncButOrdered(key, new Runnable() {
@Override @Override

View file

@ -16,6 +16,9 @@
*/ */
package org.jivesoftware.smackx.muc; package org.jivesoftware.smackx.muc;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.List; import java.util.List;
import org.jivesoftware.smack.MessageListener; import org.jivesoftware.smack.MessageListener;
@ -23,8 +26,12 @@ import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.muc.MultiUserChat.MucCreateConfigFormHandle; import org.jivesoftware.smackx.muc.MultiUserChat.MucCreateConfigFormHandle;
import org.jivesoftware.smackx.muc.MultiUserChatException.MucNotJoinedException;
import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException;
import org.jivesoftware.smackx.muc.packet.MUCUser;
import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest;
@ -36,6 +43,7 @@ import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Localpart; import org.jxmpp.jid.parts.Localpart;
import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.stringprep.XmppStringprepException;
public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest {
@ -61,6 +69,24 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest {
} }
} }
@SmackIntegrationTest
public void mucJoinLeaveTest() throws XmppStringprepException, NotAMucServiceException, NoResponseException,
XMPPErrorException, NotConnectedException, InterruptedException, MucNotJoinedException {
EntityBareJid mucAddress = JidCreate.entityBareFrom(Localpart.from("smack-inttest-join-leave-" + randomString),
mucService.getDomain());
MultiUserChat muc = mucManagerOne.getMultiUserChat(mucAddress);
muc.join(Resourcepart.from("nick-one"));
Presence reflectedLeavePresence = muc.leaveSync();
MUCUser mucUser = MUCUser.from(reflectedLeavePresence);
assertNotNull(mucUser);
assertTrue(mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110));
}
@SmackIntegrationTest @SmackIntegrationTest
public void mucTest() throws Exception { public void mucTest() throws Exception {
EntityBareJid mucAddress = JidCreate.entityBareFrom(Localpart.from("smack-inttest-" + randomString), mucService.getDomain()); EntityBareJid mucAddress = JidCreate.entityBareFrom(Localpart.from("smack-inttest-" + randomString), mucService.getDomain());

View file

@ -620,7 +620,7 @@ public class JingleSession extends JingleNegotiator implements MediaReceivedList
* A XMPP connection * A XMPP connection
* @return a Jingle session * @return a Jingle session
*/ */
public static JingleSession getInstanceFor(XMPPConnection con) { public static synchronized JingleSession getInstanceFor(XMPPConnection con) {
if (con == null) { if (con == null) {
throw new IllegalArgumentException("XMPPConnection cannot be null"); throw new IllegalArgumentException("XMPPConnection cannot be null");
} }

View file

@ -104,7 +104,9 @@ public class WorkgroupProperties extends IQ {
while (!done) { while (!done) {
int eventType = parser.next(); int eventType = parser.next();
if ((eventType == XmlPullParser.START_TAG) && ("authRequired".equals(parser.getName()))) { if ((eventType == XmlPullParser.START_TAG) && ("authRequired".equals(parser.getName()))) {
// CHECKSTYLE:OFF
props.setAuthRequired(Boolean.valueOf(parser.nextText()).booleanValue()); props.setAuthRequired(Boolean.valueOf(parser.nextText()).booleanValue());
// CHECKSTYLE:ON
} }
else if ((eventType == XmlPullParser.START_TAG) && ("email".equals(parser.getName()))) { else if ((eventType == XmlPullParser.START_TAG) && ("email".equals(parser.getName()))) {
props.setEmail(parser.nextText()); props.setEmail(parser.nextText());

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2013-2018 Florian Schmaus * Copyright 2013-2019 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,6 +41,7 @@ import org.minidns.dnsname.DnsName;
/** /**
* A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namespace. * A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namespace.
* Note that using JavaxResovler requires applications using newer Java versions (at least 11) to declare a dependency on the "sun.jdk" module.
* *
* @author Florian Schmaus * @author Florian Schmaus
* *
@ -55,8 +56,8 @@ public class JavaxResolver extends DNSResolver implements SmackInitializer {
Hashtable<String, String> env = new Hashtable<>(); Hashtable<String, String> env = new Hashtable<>();
env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
dirContext = new InitialDirContext(env); dirContext = new InitialDirContext(env);
} catch (Exception e) { } catch (NamingException e) {
// Ignore. LOGGER.log(Level.SEVERE, "Could not construct InitialDirContext", e);
} }
// Try to set this DNS resolver as primary one // Try to set this DNS resolver as primary one

View file

@ -16,10 +16,13 @@
*/ */
package org.jivesoftware.smack.sm; package org.jivesoftware.smack.sm;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.BlockingQueue;
import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.packet.Element;
import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Stanza;
public abstract class StreamManagementException extends SmackException { public abstract class StreamManagementException extends SmackException {
@ -110,5 +113,56 @@ public abstract class StreamManagementException extends SmackException {
return ackedStanzas; return ackedStanzas;
} }
} }
public static final class UnacknowledgedQueueFullException extends StreamManagementException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final int overflowElementNum;
private final int droppedElements;
private final List<Element> elements;
private final List<Stanza> unacknowledgesStanzas;
private UnacknowledgedQueueFullException(String message, int overflowElementNum, int droppedElements, List<Element> elements,
List<Stanza> unacknowledgesStanzas) {
super(message);
this.overflowElementNum = overflowElementNum;
this.droppedElements = droppedElements;
this.elements = elements;
this.unacknowledgesStanzas = unacknowledgesStanzas;
}
public int getOverflowElementNum() {
return overflowElementNum;
}
public int getDroppedElements() {
return droppedElements;
}
public List<Element> getElements() {
return elements;
}
public List<Stanza> getUnacknowledgesStanzas() {
return unacknowledgesStanzas;
}
public static UnacknowledgedQueueFullException newWith(int overflowElementNum, List<Element> elements,
BlockingQueue<Stanza> unacknowledgedStanzas) {
final int unacknowledgesStanzasQueueSize = unacknowledgedStanzas.size();
List<Stanza> localUnacknowledgesStanzas = new ArrayList<>(unacknowledgesStanzasQueueSize);
localUnacknowledgesStanzas.addAll(unacknowledgedStanzas);
int droppedElements = elements.size() - overflowElementNum - 1;
String message = "The queue size " + unacknowledgesStanzasQueueSize + " is not able to fit another "
+ droppedElements + " potential stanzas type top-level stream-elements.";
return new UnacknowledgedQueueFullException(message, overflowElementNum, droppedElements, elements,
localUnacknowledgesStanzas);
}
}
} }

View file

@ -51,6 +51,7 @@ import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; import java.util.logging.Level;
@ -83,6 +84,8 @@ import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.SmackException.NotLoggedInException;
import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException; import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException;
import org.jivesoftware.smack.SmackException.SmackWrappedException;
import org.jivesoftware.smack.SmackFuture;
import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.SynchronizationPoint; import org.jivesoftware.smack.SynchronizationPoint;
import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnection;
@ -165,15 +168,17 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
private SSLSocket secureSocket; private SSLSocket secureSocket;
/** private final Semaphore readerWriterSemaphore = new Semaphore(2);
* Protected access level because of unit test purposes
*/
protected PacketWriter packetWriter;
/** /**
* Protected access level because of unit test purposes * Protected access level because of unit test purposes
*/ */
protected PacketReader packetReader; protected final PacketWriter packetWriter = new PacketWriter();
/**
* Protected access level because of unit test purposes
*/
protected final PacketReader packetReader = new PacketReader();
private final SynchronizationPoint<Exception> initialOpenStreamSend = new SynchronizationPoint<>( private final SynchronizationPoint<Exception> initialOpenStreamSend = new SynchronizationPoint<>(
this, "initial open stream element send to server"); this, "initial open stream element send to server");
@ -278,6 +283,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
*/ */
private final Collection<StanzaListener> stanzaAcknowledgedListeners = new ConcurrentLinkedQueue<>(); private final Collection<StanzaListener> stanzaAcknowledgedListeners = new ConcurrentLinkedQueue<>();
/**
* These listeners are invoked for every stanza that got dropped.
* <p>
* We use a {@link ConcurrentLinkedQueue} here in order to allow the listeners to remove
* themselves after they have been invoked.
* </p>
*/
private final Collection<StanzaListener> stanzaDroppedListeners = new ConcurrentLinkedQueue<>();
/** /**
* This listeners are invoked for a acknowledged stanza that has the given stanza ID. They will * This listeners are invoked for a acknowledged stanza that has the given stanza ID. They will
* only be invoked once and automatically removed after that. * only be invoked once and automatically removed after that.
@ -387,6 +401,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
SSLSession sslSession = secureSocket != null ? secureSocket.getSession() : null; SSLSession sslSession = secureSocket != null ? secureSocket.getSession() : null;
saslAuthentication.authenticate(username, password, config.getAuthzid(), sslSession); saslAuthentication.authenticate(username, password, config.getAuthzid(), sslSession);
// Wait for stream features after the authentication.
// TODO: The name of this synchronization point "maybeCompressFeaturesReceived" is not perfect. It should be
// renamed to "streamFeaturesAfterAuthenticationReceived".
maybeCompressFeaturesReceived.checkIfSuccessOrWait();
// If compression is enabled then request the server to use stream compression. XEP-170 // If compression is enabled then request the server to use stream compression. XEP-170
// recommends to perform stream compression before resource binding. // recommends to perform stream compression before resource binding.
maybeEnableCompression(); maybeEnableCompression();
@ -437,10 +456,25 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
} }
} }
} }
// (Re-)send the stanzas *after* we tried to enable SM // Inform client about failed resumption if possible, resend stanzas otherwise
// Process the stanzas synchronously so a client can re-queue them for transmission
// before it is informed about connection success
if (!stanzaDroppedListeners.isEmpty()) {
for (Stanza stanza : previouslyUnackedStanzas) {
for (StanzaListener listener : stanzaDroppedListeners) {
try {
listener.processStanza(stanza);
}
catch (InterruptedException | NotConnectedException | NotLoggedInException e) {
LOGGER.log(Level.FINER, "StanzaDroppedListener received exception", e);
}
}
}
} else {
for (Stanza stanza : previouslyUnackedStanzas) { for (Stanza stanza : previouslyUnackedStanzas) {
sendStanzaInternal(stanza); sendStanzaInternal(stanza);
} }
}
afterSuccessfulLogin(false); afterSuccessfulLogin(false);
} }
@ -468,24 +502,16 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
shutdown(false); shutdown(false);
} }
/** @Override
* Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza.
*/
public synchronized void instantShutdown() { public synchronized void instantShutdown() {
shutdown(true); shutdown(true);
} }
private void shutdown(boolean instant) { private void shutdown(boolean instant) {
if (disconnectedButResumeable) {
return;
}
// First shutdown the writer, this will result in a closing stream element getting send to // First shutdown the writer, this will result in a closing stream element getting send to
// the server // the server
if (packetWriter != null) {
LOGGER.finer("PacketWriter shutdown()"); LOGGER.finer("PacketWriter shutdown()");
packetWriter.shutdown(instant); packetWriter.shutdown(instant);
}
LOGGER.finer("PacketWriter has been shut down"); LOGGER.finer("PacketWriter has been shut down");
if (!instant) { if (!instant) {
@ -500,19 +526,29 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
} }
} }
if (packetReader != null) {
LOGGER.finer("PacketReader shutdown()"); LOGGER.finer("PacketReader shutdown()");
packetReader.shutdown(); packetReader.shutdown();
}
LOGGER.finer("PacketReader has been shut down"); LOGGER.finer("PacketReader has been shut down");
final Socket socket = this.socket;
if (socket != null && socket.isConnected()) {
try { try {
socket.close(); socket.close();
} catch (Exception e) { } catch (Exception e) {
LOGGER.log(Level.WARNING, "shutdown", e); LOGGER.log(Level.WARNING, "shutdown", e);
} }
}
setWasAuthenticated(); setWasAuthenticated();
// Wait for reader and writer threads to be terminated.
readerWriterSemaphore.acquireUninterruptibly(2);
readerWriterSemaphore.release(2);
if (disconnectedButResumeable) {
return;
}
// If we are able to resume the stream, then don't set // If we are able to resume the stream, then don't set
// connected/authenticated/usingTLS to false since we like behave like we are still // connected/authenticated/usingTLS to false since we like behave like we are still
// connected (e.g. sendStanza should not throw a NotConnectedException). // connected (e.g. sendStanza should not throw a NotConnectedException).
@ -523,6 +559,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
// Reset the stream management session id to null, since if the stream is cleanly closed, i.e. sending a closing // Reset the stream management session id to null, since if the stream is cleanly closed, i.e. sending a closing
// stream tag, there is no longer a stream to resume. // stream tag, there is no longer a stream to resume.
smSessionId = null; smSessionId = null;
// Note that we deliberately do not reset authenticatedConnectionInitiallyEstablishedTimestamp here, so that the
// information is available in the connectionClosedOnError() listeners.
} }
authenticated = false; authenticated = false;
connected = false; connected = false;
@ -530,6 +568,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
reader = null; reader = null;
writer = null; writer = null;
initState();
}
@Override
protected void initState() {
super.initState();
maybeCompressFeaturesReceived.init(); maybeCompressFeaturesReceived.init();
compressSyncPoint.init(); compressSyncPoint.init();
smResumedSyncPoint.init(); smResumedSyncPoint.init();
@ -555,7 +599,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
} }
} }
private void connectUsingConfiguration() throws ConnectionException, IOException { private void connectUsingConfiguration() throws ConnectionException, IOException, InterruptedException {
List<HostAddress> failedAddresses = populateHostAddresses(); List<HostAddress> failedAddresses = populateHostAddresses();
SocketFactory socketFactory = config.getSocketFactory(); SocketFactory socketFactory = config.getSocketFactory();
ProxyInfo proxyInfo = config.getProxyInfo(); ProxyInfo proxyInfo = config.getProxyInfo();
@ -574,14 +618,16 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
innerloop: while (inetAddresses.hasNext()) { innerloop: while (inetAddresses.hasNext()) {
// Create a *new* Socket before every connection attempt, i.e. connect() call, since Sockets are not // Create a *new* Socket before every connection attempt, i.e. connect() call, since Sockets are not
// re-usable after a failed connection attempt. See also SMACK-724. // re-usable after a failed connection attempt. See also SMACK-724.
socket = socketFactory.createSocket(); SmackFuture.SocketFuture socketFuture = new SmackFuture.SocketFuture(socketFactory);
final InetAddress inetAddress = inetAddresses.next(); final InetAddress inetAddress = inetAddresses.next();
final String inetAddressAndPort = inetAddress + " at port " + port; final InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, port);
LOGGER.finer("Trying to establish TCP connection to " + inetAddressAndPort); LOGGER.finer("Trying to establish TCP connection to " + inetSocketAddress);
socketFuture.connectAsync(inetSocketAddress, timeout);
try { try {
socket.connect(new InetSocketAddress(inetAddress, port), timeout); socket = socketFuture.getOrThrow();
} catch (Exception e) { } catch (IOException e) {
hostAddress.setException(inetAddress, e); hostAddress.setException(inetAddress, e);
if (inetAddresses.hasNext()) { if (inetAddresses.hasNext()) {
continue innerloop; continue innerloop;
@ -589,7 +635,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
break innerloop; break innerloop;
} }
} }
LOGGER.finer("Established TCP connection to " + inetAddressAndPort); LOGGER.finer("Established TCP connection to " + inetSocketAddress);
// We found a host to connect to, return here // We found a host to connect to, return here
this.host = host; this.host = host;
this.port = port; this.port = port;
@ -605,6 +651,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
proxyInfo.getProxySocketConnection().connect(socket, host, port, timeout); proxyInfo.getProxySocketConnection().connect(socket, host, port, timeout);
} catch (IOException e) { } catch (IOException e) {
hostAddress.setException(e); hostAddress.setException(e);
failedAddresses.add(hostAddress);
continue; continue;
} }
LOGGER.finer("Established TCP connection to " + hostAndPort); LOGGER.finer("Established TCP connection to " + hostAndPort);
@ -627,18 +674,23 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
* @throws XMPPException if establishing a connection to the server fails. * @throws XMPPException if establishing a connection to the server fails.
* @throws SmackException if the server fails to respond back or if there is anther error. * @throws SmackException if the server fails to respond back or if there is anther error.
* @throws IOException * @throws IOException
* @throws InterruptedException
*/ */
private void initConnection() throws IOException { private void initConnection() throws IOException, InterruptedException {
boolean isFirstInitialization = packetReader == null || packetWriter == null;
compressionHandler = null; compressionHandler = null;
// Set the reader and writer instance variables // Set the reader and writer instance variables
initReaderAndWriter(); initReaderAndWriter();
if (isFirstInitialization) { int availableReaderWriterSemaphorePermits = readerWriterSemaphore.availablePermits();
packetWriter = new PacketWriter(); if (availableReaderWriterSemaphorePermits < 2) {
packetReader = new PacketReader(); Object[] logObjects = new Object[] {
this,
availableReaderWriterSemaphorePermits,
};
LOGGER.log(Level.FINE, "Not every reader/writer threads where terminated on connection re-initializtion of {0}. Available permits {1}", logObjects);
} }
readerWriterSemaphore.acquire(2);
// Start the writer thread. This will open an XMPP stream to the server // Start the writer thread. This will open an XMPP stream to the server
packetWriter.init(); packetWriter.init();
// Start the reader thread. The startup() method will block until we // Start the reader thread. The startup() method will block until we
@ -859,7 +911,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
if (!config.isCompressionEnabled()) { if (!config.isCompressionEnabled()) {
return; return;
} }
maybeCompressFeaturesReceived.checkIfSuccessOrWait();
Compress.Feature compression = getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE); Compress.Feature compression = getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE);
if (compression == null) { if (compression == null) {
// Server does not support compression // Server does not support compression
@ -910,19 +962,43 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
* *
* @param e the exception that causes the connection close event. * @param e the exception that causes the connection close event.
*/ */
private synchronized void notifyConnectionError(Exception e) { private void notifyConnectionError(final Exception e) {
ASYNC_BUT_ORDERED.performAsyncButOrdered(this, new Runnable() {
@Override
public void run() {
// Listeners were already notified of the exception, return right here. // Listeners were already notified of the exception, return right here.
if ((packetReader == null || packetReader.done) && if (packetReader.done || packetWriter.done()) return;
(packetWriter == null || packetWriter.done())) return;
// Report the failure outside the synchronized block, so that a thread waiting within a synchronized
// function like connect() throws the wrapped exception.
SmackWrappedException smackWrappedException = new SmackWrappedException(e);
tlsHandled.reportGenericFailure(smackWrappedException);
saslFeatureReceived.reportGenericFailure(smackWrappedException);
maybeCompressFeaturesReceived.reportGenericFailure(smackWrappedException);
lastFeaturesReceived.reportGenericFailure(smackWrappedException);
synchronized (XMPPTCPConnection.this) {
// Within this synchronized block, either *both* reader and writer threads must be terminated, or
// none.
assert ((packetReader.done && packetWriter.done())
|| (!packetReader.done && !packetWriter.done()));
// Closes the connection temporary. A reconnection is possible // Closes the connection temporary. A reconnection is possible
// Note that a connection listener of XMPPTCPConnection will drop the SM state in // Note that a connection listener of XMPPTCPConnection will drop the SM state in
// case the Exception is a StreamErrorException. // case the Exception is a StreamErrorException.
instantShutdown(); instantShutdown();
}
Async.go(new Runnable() {
@Override
public void run() {
// Notify connection listeners of the error. // Notify connection listeners of the error.
callConnectionClosedOnErrorListener(e); callConnectionClosedOnErrorListener(e);
} }
}, XMPPTCPConnection.this + " callConnectionClosedOnErrorListener()");
}
});
}
/** /**
* For unit testing purposes * For unit testing purposes
@ -934,14 +1010,13 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
} }
@Override @Override
protected void afterFeaturesReceived() throws NotConnectedException, InterruptedException { protected void afterFeaturesReceived() throws NotConnectedException, InterruptedException, SecurityRequiredByServerException {
StartTls startTlsFeature = getFeature(StartTls.ELEMENT, StartTls.NAMESPACE); StartTls startTlsFeature = getFeature(StartTls.ELEMENT, StartTls.NAMESPACE);
if (startTlsFeature != null) { if (startTlsFeature != null) {
if (startTlsFeature.required() && config.getSecurityMode() == SecurityMode.disabled) { if (startTlsFeature.required() && config.getSecurityMode() == SecurityMode.disabled) {
SmackException smackException = new SecurityRequiredByServerException(); SecurityRequiredByServerException smackException = new SecurityRequiredByServerException();
tlsHandled.reportFailure(smackException); tlsHandled.reportFailure(smackException);
notifyConnectionError(smackException); throw smackException;
return;
} }
if (config.getSecurityMode() != ConnectionConfiguration.SecurityMode.disabled) { if (config.getSecurityMode() != ConnectionConfiguration.SecurityMode.disabled) {
@ -992,6 +1067,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
protected class PacketReader { protected class PacketReader {
private final String threadName = "Smack Reader (" + getConnectionCounter() + ')';
XmlPullParser parser; XmlPullParser parser;
private volatile boolean done; private volatile boolean done;
@ -1006,9 +1083,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
Async.go(new Runnable() { Async.go(new Runnable() {
@Override @Override
public void run() { public void run() {
LOGGER.finer(threadName + " start");
try {
parsePackets(); parsePackets();
} finally {
LOGGER.finer(threadName + " exit");
XMPPTCPConnection.this.readerWriterSemaphore.release();
} }
}, "Smack Reader (" + getConnectionCounter() + ")"); }
}, threadName);
} }
/** /**
@ -1228,7 +1311,11 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
LOGGER.info(XMPPTCPConnection.this LOGGER.info(XMPPTCPConnection.this
+ " received closing </stream> element." + " received closing </stream> element."
+ " Server wants to terminate the connection, calling disconnect()"); + " Server wants to terminate the connection, calling disconnect()");
ASYNC_BUT_ORDERED.performAsyncButOrdered(XMPPTCPConnection.this, new Runnable() {
@Override
public void run() {
disconnect(); disconnect();
}});
} }
} }
break; break;
@ -1257,6 +1344,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
protected class PacketWriter { protected class PacketWriter {
public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE; public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE;
private final String threadName = "Smack Writer (" + getConnectionCounter() + ')';
private final ArrayBlockingQueueWithShutdown<Element> queue = new ArrayBlockingQueueWithShutdown<>( private final ArrayBlockingQueueWithShutdown<Element> queue = new ArrayBlockingQueueWithShutdown<>(
QUEUE_SIZE, true); QUEUE_SIZE, true);
@ -1302,9 +1391,15 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
Async.go(new Runnable() { Async.go(new Runnable() {
@Override @Override
public void run() { public void run() {
LOGGER.finer(threadName + " start");
try {
writePackets(); writePackets();
} finally {
LOGGER.finer(threadName + " exit");
XMPPTCPConnection.this.readerWriterSemaphore.release();
} }
}, "Smack Writer (" + getConnectionCounter() + ")"); }
}, threadName);
} }
private boolean done() { private boolean done() {
@ -1355,13 +1450,14 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
instantShutdown = instant; instantShutdown = instant;
queue.shutdown(); queue.shutdown();
shutdownTimestamp = System.currentTimeMillis(); shutdownTimestamp = System.currentTimeMillis();
if (shutdownDone.isNotInInitialState()) {
try { try {
shutdownDone.checkIfSuccessOrWait(); shutdownDone.checkIfSuccessOrWait();
} } catch (NoResponseException | InterruptedException e) {
catch (NoResponseException | InterruptedException e) {
LOGGER.log(Level.WARNING, "shutdownDone was not marked as successful by the writer thread", e); LOGGER.log(Level.WARNING, "shutdownDone was not marked as successful by the writer thread", e);
} }
} }
}
/** /**
* Maybe return the next available element from the queue for writing. If the queue is shut down <b>or</b> a * Maybe return the next available element from the queue for writing. If the queue is shut down <b>or</b> a
@ -1514,7 +1610,16 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
private void drainWriterQueueToUnacknowledgedStanzas() { private void drainWriterQueueToUnacknowledgedStanzas() {
List<Element> elements = new ArrayList<>(queue.size()); List<Element> elements = new ArrayList<>(queue.size());
queue.drainTo(elements); queue.drainTo(elements);
for (Element element : elements) { for (int i = 0; i < elements.size(); i++) {
Element element = elements.get(i);
// If the unacknowledgedStanza queue is full, then bail out with a warning message. See SMACK-844.
if (unacknowledgedStanzas.remainingCapacity() == 0) {
StreamManagementException.UnacknowledgedQueueFullException exception = StreamManagementException.UnacknowledgedQueueFullException
.newWith(i, elements, unacknowledgedStanzas);
LOGGER.log(Level.WARNING,
"Some stanzas may be lost as not all could be drained to the unacknowledged stanzas queue", exception);
return;
}
if (element instanceof Stanza) { if (element instanceof Stanza) {
unacknowledgedStanzas.add((Stanza) element); unacknowledgedStanzas.add((Stanza) element);
} }
@ -1719,6 +1824,32 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
stanzaAcknowledgedListeners.clear(); stanzaAcknowledgedListeners.clear();
} }
/**
* Add a Stanza dropped listener.
* <p>
* Those listeners will be invoked every time a Stanza has been dropped due to a failed SM resume. They will not get
* automatically removed. If at least one StanzaDroppedListener is configured, no attempt will be made to retransmit
* the Stanzas.
* </p>
*
* @param listener the listener to add.
* @since 4.3.3
*/
public void addStanzaDroppedListener(StanzaListener listener) {
stanzaDroppedListeners.add(listener);
}
/**
* Remove the given Stanza dropped listener.
*
* @param listener the listener.
* @return true if the listener was removed.
* @since 4.3.3
*/
public boolean removeStanzaDroppedListener(StanzaListener listener) {
return stanzaDroppedListeners.remove(listener);
}
/** /**
* Add a new Stanza ID acknowledged listener for the given ID. * Add a new Stanza ID acknowledged listener for the given ID.
* <p> * <p>

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2014 Florian Schmaus * Copyright 2014-2019 Florian Schmaus
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -49,11 +49,9 @@ public class PacketWriterTest {
@Test @Test
public void shouldBlockAndUnblockTest() throws InterruptedException, BrokenBarrierException, NotConnectedException, XmppStringprepException { public void shouldBlockAndUnblockTest() throws InterruptedException, BrokenBarrierException, NotConnectedException, XmppStringprepException {
XMPPTCPConnection connection = new XMPPTCPConnection("user", "pass", "example.org"); XMPPTCPConnection connection = new XMPPTCPConnection("user", "pass", "example.org");
final PacketWriter pw = connection.new PacketWriter(); final PacketWriter pw = connection.packetWriter;
connection.packetWriter = pw;
connection.packetReader = connection.new PacketReader();
connection.setWriter(new BlockingStringWriter()); connection.setWriter(new BlockingStringWriter());
pw.init(); connection.packetWriter.init();
for (int i = 0; i < XMPPTCPConnection.PacketWriter.QUEUE_SIZE; i++) { for (int i = 0; i < XMPPTCPConnection.PacketWriter.QUEUE_SIZE; i++) {
pw.sendStreamElement(new Message()); pw.sendStreamElement(new Message());

View file

@ -1,9 +1,15 @@
allprojects { allprojects {
ext { ext {
shortVersion = '4.3.1' shortVersion = '4.3.4'
isSnapshot = true isSnapshot = true
jxmppVersion = '0.6.3' // When using dynamic versions for those, do *not* use [1.0,
miniDnsVersion = '0.3.2' // 2.0), since this will also pull in 2.0-alpha1. Instead use
// [1.0, 1.0.99].
// See also:
// - https://issues.apache.org/jira/browse/MNG-6232
// - https://issues.igniterealtime.org/browse/SMACK-858
jxmppVersion = '[0.6, 0.6.999]'
miniDnsVersion = '[0.3, 0.3.999]'
smackMinAndroidSdk = 9 smackMinAndroidSdk = 9
} }
} }