Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorslewis2006-04-14 04:45:38 +0000
committerslewis2006-04-14 04:45:38 +0000
commit50a245a0891b6870dcd83b667c8364d5741238d6 (patch)
tree91b890df2f0122297450b519f56e7e16aabd7993 /protocols
parent917f444b93cb02a5e333041e1336af83ccdbc110 (diff)
downloadorg.eclipse.ecf-50a245a0891b6870dcd83b667c8364d5741238d6.tar.gz
org.eclipse.ecf-50a245a0891b6870dcd83b667c8364d5741238d6.tar.xz
org.eclipse.ecf-50a245a0891b6870dcd83b667c8364d5741238d6.zip
Added plugin org.jivesoftware.smack plugin (v 2.2.0). Changed dependencies in org.eclipse.ecf.provider.xmpp to this new plugin rather than to smack libraries in org.eclipse.ecf.provider.xmpp/lib. Added smack plugin to build. Changed xmpp feature to include smack plugin.
Diffstat (limited to 'protocols')
-rw-r--r--protocols/bundles/org.jivesoftware.smack/.classpath10
-rw-r--r--protocols/bundles/org.jivesoftware.smack/.project28
-rw-r--r--protocols/bundles/org.jivesoftware.smack/.settings/org.eclipse.jdt.core.prefs7
-rw-r--r--protocols/bundles/org.jivesoftware.smack/META-INF/MANIFEST.MF28
-rw-r--r--protocols/bundles/org.jivesoftware.smack/build.properties12
-rw-r--r--protocols/bundles/org.jivesoftware.smack/jars/smack.jarbin0 -> 178866 bytes
-rw-r--r--protocols/bundles/org.jivesoftware.smack/jars/smackx-debug.jarbin0 -> 52905 bytes
-rw-r--r--protocols/bundles/org.jivesoftware.smack/jars/smackx.jarbin0 -> 306783 bytes
-rw-r--r--protocols/bundles/org.jivesoftware.smack/jars/xmlpull.v1.jarbin0 -> 32679 bytes
-rw-r--r--protocols/bundles/org.jivesoftware.smack/plugin.xml6
-rw-r--r--protocols/bundles/org.jivesoftware.smack/schema/debuggers.exsd99
-rw-r--r--protocols/bundles/org.jivesoftware.smack/schema/providerManagers.exsd99
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/META-INF/smack-config.xml19
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/META-INF/smack.providers183
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/AccountManager.java298
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Chat.java270
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionConfiguration.java357
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionEstablishedListener.java41
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener.java45
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener2.java10
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GoogleTalkConnection.java38
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GroupChat.java353
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/NonSASLAuthentication.java128
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/OpenTrustManager.java49
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketCollector.java185
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketInterceptor.java49
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketListener.java48
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketReader.java854
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketWriter.java451
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Roster.java813
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterEntry.java193
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterGroup.java252
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterListener.java62
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SASLAuthentication.java417
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SSLXMPPConnection.java160
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ServerTrustManager.java184
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SmackConfiguration.java207
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/UserAuthentication.java55
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPConnection.java1249
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPException.java219
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java161
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/LiteDebugger.java336
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/SmackDebugger.java98
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/AndFilter.java103
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromContainsFilter.java54
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromMatchesFilter.java71
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/IQTypeFilter.java48
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/MessageTypeFilter.java54
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/NotFilter.java50
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/OrFilter.java103
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java51
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketFilter.java63
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketIDFilter.java49
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketTypeFilter.java58
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ThreadFilter.java55
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ToContainsFilter.java55
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Authentication.java186
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Bind.java71
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java134
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/IQ.java167
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Message.java273
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Packet.java423
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/PacketExtension.java56
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Presence.java327
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Registration.java113
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/RosterPacket.java348
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Session.java45
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/StreamError.java106
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/XMPPError.java117
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/IQProvider.java47
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java46
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/ProviderManager.java396
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLAnonymous.java52
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLMechanism.java122
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java58
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/Cache.java628
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/DNSUtil.java219
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableReader.java118
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableWriter.java120
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/PacketParserUtils.java417
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ReaderListener.java41
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/StringUtils.java432
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/WriterListener.java41
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java55
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/Form.java539
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/FormField.java354
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/GroupChatInvitation.java115
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventManager.java304
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventNotificationListener.java74
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventRequestListener.java86
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientInfo.java98
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientManager.java353
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/NodeInformationProvider.java44
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageHeader.java85
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageManager.java284
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/PrivateDataManager.java345
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RemoteRosterEntry.java118
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ReportedData.java277
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeListener.java42
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeManager.java177
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java483
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/SharedGroupManager.java53
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLManager.java141
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLText.java429
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebugger.java985
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java377
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Base64.java1416
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java130
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java353
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java36
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java178
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java438
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java138
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java457
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java187
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java364
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java777
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java164
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Affiliate.java98
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java76
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java79
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java70
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DiscussionHistory.java173
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/HostedRoom.java65
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationListener.java49
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java38
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/MultiUserChat.java2668
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Occupant.java104
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java179
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/RoomInfo.java184
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java38
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/UserStatusListener.java127
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Bytestream.java481
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DataForm.java296
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java137
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DelayInformation.java146
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverInfo.java268
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverItems.java235
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/IBBExtensions.java241
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/LastActivity.java158
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCAdmin.java234
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java223
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCOwner.java339
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCUser.java627
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MessageEvent.java334
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MultipleAddresses.java205
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java128
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java237
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/PrivateData.java52
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/RosterExchange.java175
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java73
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/StreamInitiation.java419
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Time.java196
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/VCard.java798
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Version.java132
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/XHTMLExtension.java123
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/BytestreamsProvider.java104
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DataFormProvider.java160
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java75
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java83
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java71
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/IBBProviders.java85
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java81
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java108
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCUserProvider.java174
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MessageEventProvider.java77
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java67
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java46
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java90
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java112
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/VCardProvider.java244
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java78
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/package.html1
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/SimpleUserSearch.java145
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearch.java249
-rw-r--r--protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearchManager.java109
187 files changed, 36821 insertions, 0 deletions
diff --git a/protocols/bundles/org.jivesoftware.smack/.classpath b/protocols/bundles/org.jivesoftware.smack/.classpath
new file mode 100644
index 000000000..ae0ea80f3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/.classpath
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry exported="true" kind="lib" path="jars/xmlpull.v1.jar"/>
+ <classpathentry exported="true" kind="lib" path="jars/smack.jar"/>
+ <classpathentry exported="true" kind="lib" path="jars/smackx.jar"/>
+ <classpathentry exported="true" kind="lib" path="jars/smackx-debug.jar"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/executionEnvironments/J2SE-1.4"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/protocols/bundles/org.jivesoftware.smack/.project b/protocols/bundles/org.jivesoftware.smack/.project
new file mode 100644
index 000000000..247d8cc4e
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.jivesoftware.smack</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/protocols/bundles/org.jivesoftware.smack/.settings/org.eclipse.jdt.core.prefs b/protocols/bundles/org.jivesoftware.smack/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 000000000..9a51ee3e8
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,7 @@
+#Tue Apr 11 23:32:11 PDT 2006
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.2
+org.eclipse.jdt.core.compiler.compliance=1.4
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning
+org.eclipse.jdt.core.compiler.source=1.3
diff --git a/protocols/bundles/org.jivesoftware.smack/META-INF/MANIFEST.MF b/protocols/bundles/org.jivesoftware.smack/META-INF/MANIFEST.MF
new file mode 100644
index 000000000..410f7fe20
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/META-INF/MANIFEST.MF
@@ -0,0 +1,28 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: ECF Smack Plug-in
+Bundle-SymbolicName: org.jivesoftware.smack;singleton:=true
+Bundle-Version: 2.2.0
+Bundle-Vendor: eclipsercp.org
+Bundle-Localization: plugin
+Export-Package: org.jivesoftware.smack,
+ org.jivesoftware.smack.debugger,
+ org.jivesoftware.smack.filter,
+ org.jivesoftware.smack.packet,
+ org.jivesoftware.smack.provider,
+ org.jivesoftware.smack.sasl,
+ org.jivesoftware.smack.util,
+ org.jivesoftware.smackx,
+ org.jivesoftware.smackx.debugger,
+ org.jivesoftware.smackx.filetransfer,
+ org.jivesoftware.smackx.muc,
+ org.jivesoftware.smackx.packet,
+ org.jivesoftware.smackx.provider,
+ org.jivesoftware.smackx.search,
+ org.xmlpull.mxp1,
+ org.xmlpull.v1
+Require-Bundle: org.eclipse.core.runtime
+Bundle-ClassPath: jars/xmlpull.v1.jar,
+ jars/smack.jar,
+ jars/smackx.jar,
+ jars/smackx-debug.jar
diff --git a/protocols/bundles/org.jivesoftware.smack/build.properties b/protocols/bundles/org.jivesoftware.smack/build.properties
new file mode 100644
index 000000000..dcf50694d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/build.properties
@@ -0,0 +1,12 @@
+bin.includes = META-INF/,\
+ plugin.xml,\
+ schema/,\
+ jars/
+jars.compile.order =
+src.includes = schema/,\
+ plugin.xml,\
+ META-INF/,\
+ jars/
+jre.compilation.profile = J2SE-1.4
+bin.excludes = jars/jsse.jar
+src.excludes = jars/jsse.jar
diff --git a/protocols/bundles/org.jivesoftware.smack/jars/smack.jar b/protocols/bundles/org.jivesoftware.smack/jars/smack.jar
new file mode 100644
index 000000000..dfe9d8426
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/jars/smack.jar
Binary files differ
diff --git a/protocols/bundles/org.jivesoftware.smack/jars/smackx-debug.jar b/protocols/bundles/org.jivesoftware.smack/jars/smackx-debug.jar
new file mode 100644
index 000000000..ff9c58781
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/jars/smackx-debug.jar
Binary files differ
diff --git a/protocols/bundles/org.jivesoftware.smack/jars/smackx.jar b/protocols/bundles/org.jivesoftware.smack/jars/smackx.jar
new file mode 100644
index 000000000..00eeeaeb3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/jars/smackx.jar
Binary files differ
diff --git a/protocols/bundles/org.jivesoftware.smack/jars/xmlpull.v1.jar b/protocols/bundles/org.jivesoftware.smack/jars/xmlpull.v1.jar
new file mode 100644
index 000000000..937c885a3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/jars/xmlpull.v1.jar
Binary files differ
diff --git a/protocols/bundles/org.jivesoftware.smack/plugin.xml b/protocols/bundles/org.jivesoftware.smack/plugin.xml
new file mode 100644
index 000000000..376d401db
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/plugin.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.0"?>
+<plugin>
+ <extension-point id="providerManagers" name="Smack Extension/IQ Provider Managers" schema="schema/providerManagers.exsd"/>
+ <extension-point id="debuggers" name="Smack Debugger" schema="schema/debuggers.exsd"/>
+</plugin>
diff --git a/protocols/bundles/org.jivesoftware.smack/schema/debuggers.exsd b/protocols/bundles/org.jivesoftware.smack/schema/debuggers.exsd
new file mode 100644
index 000000000..b6ec4cd4d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/schema/debuggers.exsd
@@ -0,0 +1,99 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- Schema file written by PDE -->
+<schema targetNamespace="org.jivesoftware.smack">
+<annotation>
+ <appInfo>
+ <meta.schema plugin="org.jivesoftware.smack" id="debuggers" name="Smack Debuggers"/>
+ </appInfo>
+ <documentation>
+ [Enter description of this extension point.]
+ </documentation>
+ </annotation>
+
+ <element name="extension">
+ <complexType>
+ <sequence>
+ <element ref="debugger"/>
+ </sequence>
+ <attribute name="point" type="string" use="required">
+ <annotation>
+ <documentation>
+
+ </documentation>
+ </annotation>
+ </attribute>
+ <attribute name="id" type="string">
+ <annotation>
+ <documentation>
+
+ </documentation>
+ </annotation>
+ </attribute>
+ <attribute name="name" type="string">
+ <annotation>
+ <documentation>
+
+ </documentation>
+ </annotation>
+ </attribute>
+ </complexType>
+ </element>
+
+ <element name="debugger">
+ <complexType>
+ <attribute name="class" type="string" use="required">
+ <annotation>
+ <documentation>
+
+ </documentation>
+ </annotation>
+ </attribute>
+ </complexType>
+ </element>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="since"/>
+ </appInfo>
+ <documentation>
+ [Enter the first release in which this extension point appears.]
+ </documentation>
+ </annotation>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="examples"/>
+ </appInfo>
+ <documentation>
+ [Enter extension point usage example here.]
+ </documentation>
+ </annotation>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="apiInfo"/>
+ </appInfo>
+ <documentation>
+ [Enter API information here.]
+ </documentation>
+ </annotation>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="implementation"/>
+ </appInfo>
+ <documentation>
+ [Enter information about supplied implementation of this extension point.]
+ </documentation>
+ </annotation>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="copyright"/>
+ </appInfo>
+ <documentation>
+
+ </documentation>
+ </annotation>
+
+</schema>
diff --git a/protocols/bundles/org.jivesoftware.smack/schema/providerManagers.exsd b/protocols/bundles/org.jivesoftware.smack/schema/providerManagers.exsd
new file mode 100644
index 000000000..2822b5238
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/schema/providerManagers.exsd
@@ -0,0 +1,99 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- Schema file written by PDE -->
+<schema targetNamespace="org.jivesoftware.smack">
+<annotation>
+ <appInfo>
+ <meta.schema plugin="org.jivesoftware.smack" id="providerManagers" name="Smack Extension/IQ Provider Managers"/>
+ </appInfo>
+ <documentation>
+ [Enter description of this extension point.]
+ </documentation>
+ </annotation>
+
+ <element name="extension">
+ <complexType>
+ <sequence>
+ <element ref="manager"/>
+ </sequence>
+ <attribute name="point" type="string" use="required">
+ <annotation>
+ <documentation>
+
+ </documentation>
+ </annotation>
+ </attribute>
+ <attribute name="id" type="string">
+ <annotation>
+ <documentation>
+
+ </documentation>
+ </annotation>
+ </attribute>
+ <attribute name="name" type="string">
+ <annotation>
+ <documentation>
+
+ </documentation>
+ </annotation>
+ </attribute>
+ </complexType>
+ </element>
+
+ <element name="manager">
+ <complexType>
+ <attribute name="class" type="string" use="required">
+ <annotation>
+ <documentation>
+
+ </documentation>
+ </annotation>
+ </attribute>
+ </complexType>
+ </element>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="since"/>
+ </appInfo>
+ <documentation>
+ [Enter the first release in which this extension point appears.]
+ </documentation>
+ </annotation>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="examples"/>
+ </appInfo>
+ <documentation>
+ [Enter extension point usage example here.]
+ </documentation>
+ </annotation>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="apiInfo"/>
+ </appInfo>
+ <documentation>
+ [Enter API information here.]
+ </documentation>
+ </annotation>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="implementation"/>
+ </appInfo>
+ <documentation>
+ [Enter information about supplied implementation of this extension point.]
+ </documentation>
+ </annotation>
+
+ <annotation>
+ <appInfo>
+ <meta.section type="copyright"/>
+ </appInfo>
+ <documentation>
+
+ </documentation>
+ </annotation>
+
+</schema>
diff --git a/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack-config.xml b/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack-config.xml
new file mode 100644
index 000000000..eb0ea25ce
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack-config.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<!-- Smack configuration file. -->
+<smack>
+
+ <!-- Classes that will be loaded when Smack starts -->
+ <startupClasses>
+ <className>org.jivesoftware.smackx.ServiceDiscoveryManager</className>
+ <className>org.jivesoftware.smackx.XHTMLManager</className>
+ <className>org.jivesoftware.smackx.muc.MultiUserChat</className>
+ <className>org.jivesoftware.smackx.filetransfer.FileTransferManager</className>
+ </startupClasses>
+
+ <!-- Paket reply timeout in milliseconds -->
+ <packetReplyTimeout>5000</packetReplyTimeout>
+
+ <!-- Keep-alive interval in milleseconds -->
+ <keepAliveInterval>30000</keepAliveInterval>
+
+</smack> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack.providers b/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack.providers
new file mode 100644
index 000000000..98867a4fa
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack.providers
@@ -0,0 +1,183 @@
+<?xml version="1.0"?>
+<!-- Providers file for default Smack extensions -->
+<smackProviders>
+
+ <!-- Private Data Storage -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>jabber:iq:private</namespace>
+ <className>org.jivesoftware.smackx.PrivateDataManager$PrivateDataIQProvider</className>
+ </iqProvider>
+
+ <!-- Time -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>jabber:iq:time</namespace>
+ <className>org.jivesoftware.smackx.packet.Time</className>
+ </iqProvider>
+
+ <!-- Roster Exchange -->
+ <extensionProvider>
+ <elementName>x</elementName>
+ <namespace>jabber:x:roster</namespace>
+ <className>org.jivesoftware.smackx.provider.RosterExchangeProvider</className>
+ </extensionProvider>
+
+ <!-- Message Events -->
+ <extensionProvider>
+ <elementName>x</elementName>
+ <namespace>jabber:x:event</namespace>
+ <className>org.jivesoftware.smackx.provider.MessageEventProvider</className>
+ </extensionProvider>
+
+ <!-- XHTML -->
+ <extensionProvider>
+ <elementName>html</elementName>
+ <namespace>http://jabber.org/protocol/xhtml-im</namespace>
+ <className>org.jivesoftware.smackx.provider.XHTMLExtensionProvider</className>
+ </extensionProvider>
+
+ <!-- Group Chat Invitations -->
+ <extensionProvider>
+ <elementName>x</elementName>
+ <namespace>jabber:x:conference</namespace>
+ <className>org.jivesoftware.smackx.GroupChatInvitation$Provider</className>
+ </extensionProvider>
+
+ <!-- Service Discovery # Items -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>http://jabber.org/protocol/disco#items</namespace>
+ <className>org.jivesoftware.smackx.provider.DiscoverItemsProvider</className>
+ </iqProvider>
+
+ <!-- Service Discovery # Info -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>http://jabber.org/protocol/disco#info</namespace>
+ <className>org.jivesoftware.smackx.provider.DiscoverInfoProvider</className>
+ </iqProvider>
+
+ <!-- Data Forms-->
+ <extensionProvider>
+ <elementName>x</elementName>
+ <namespace>jabber:x:data</namespace>
+ <className>org.jivesoftware.smackx.provider.DataFormProvider</className>
+ </extensionProvider>
+
+ <!-- MUC User -->
+ <extensionProvider>
+ <elementName>x</elementName>
+ <namespace>http://jabber.org/protocol/muc#user</namespace>
+ <className>org.jivesoftware.smackx.provider.MUCUserProvider</className>
+ </extensionProvider>
+
+ <!-- MUC Admin -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>http://jabber.org/protocol/muc#admin</namespace>
+ <className>org.jivesoftware.smackx.provider.MUCAdminProvider</className>
+ </iqProvider>
+
+ <!-- MUC Owner -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>http://jabber.org/protocol/muc#owner</namespace>
+ <className>org.jivesoftware.smackx.provider.MUCOwnerProvider</className>
+ </iqProvider>
+
+ <!-- Delayed Delivery -->
+ <extensionProvider>
+ <elementName>x</elementName>
+ <namespace>jabber:x:delay</namespace>
+ <className>org.jivesoftware.smackx.provider.DelayInformationProvider</className>
+ </extensionProvider>
+
+ <!-- Version -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>jabber:iq:version</namespace>
+ <className>org.jivesoftware.smackx.packet.Version</className>
+ </iqProvider>
+
+ <!-- VCard -->
+ <iqProvider>
+ <elementName>vCard</elementName>
+ <namespace>vcard-temp</namespace>
+ <className>org.jivesoftware.smackx.provider.VCardProvider</className>
+ </iqProvider>
+
+ <!-- Offline Message Requests -->
+ <iqProvider>
+ <elementName>offline</elementName>
+ <namespace>http://jabber.org/protocol/offline</namespace>
+ <className>org.jivesoftware.smackx.packet.OfflineMessageRequest$Provider</className>
+ </iqProvider>
+
+ <!-- Offline Message Indicator -->
+ <extensionProvider>
+ <elementName>offline</elementName>
+ <namespace>http://jabber.org/protocol/offline</namespace>
+ <className>org.jivesoftware.smackx.packet.OfflineMessageInfo$Provider</className>
+ </extensionProvider>
+
+ <!-- Last Activity -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>jabber:iq:last</namespace>
+ <className>org.jivesoftware.smackx.packet.LastActivity$Provider</className>
+ </iqProvider>
+
+ <!-- User Search -->
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>jabber:iq:search</namespace>
+ <className>org.jivesoftware.smackx.search.UserSearch$Provider</className>
+ </iqProvider>
+
+ <!-- SharedGroupsInfo -->
+ <iqProvider>
+ <elementName>sharedgroup</elementName>
+ <namespace>http://www.jivesoftware.org/protocol/sharedgroup</namespace>
+ <className>org.jivesoftware.smackx.packet.SharedGroupsInfo$Provider</className>
+ </iqProvider>
+
+ <!-- JEP-33: Extended Stanza Addressing -->
+ <extensionProvider>
+ <elementName>addresses</elementName>
+ <namespace>http://jabber.org/protocol/address</namespace>
+ <className>org.jivesoftware.smackx.provider.MultipleAddressesProvider</className>
+ </extensionProvider>
+
+ <!-- FileTransfer -->
+ <iqProvider>
+ <elementName>si</elementName>
+ <namespace>http://jabber.org/protocol/si</namespace>
+ <className>org.jivesoftware.smackx.provider.StreamInitiationProvider</className>
+ </iqProvider>
+
+ <iqProvider>
+ <elementName>query</elementName>
+ <namespace>http://jabber.org/protocol/bytestreams</namespace>
+ <className>org.jivesoftware.smackx.provider.BytestreamsProvider</className>
+ </iqProvider>
+
+ <iqProvider>
+ <elementName>open</elementName>
+ <namespace>http://jabber.org/protocol/ibb</namespace>
+ <className>org.jivesoftware.smackx.provider.IBBProviders$Open</className>
+ </iqProvider>
+
+ <iqProvider>
+ <elementName>close</elementName>
+ <namespace>http://jabber.org/protocol/ibb</namespace>
+ <className>org.jivesoftware.smackx.provider.IBBProviders$Close</className>
+ </iqProvider>
+
+ <extensionProvider>
+ <elementName>data</elementName>
+ <namespace>http://jabber.org/protocol/ibb</namespace>
+ <className>org.jivesoftware.smackx.provider.IBBProviders$Data</className>
+ </extensionProvider>
+
+</smackProviders> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/AccountManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/AccountManager.java
new file mode 100644
index 000000000..7fb515d7b
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/AccountManager.java
@@ -0,0 +1,298 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.Registration;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.*;
+
+/**
+ * Allows creation and management of accounts on an XMPP server.
+ *
+ * @see XMPPConnection#getAccountManager()
+ * @author Matt Tucker
+ */
+public class AccountManager {
+
+ private XMPPConnection connection;
+ private Registration info = null;
+
+ /**
+ * Creates a new AccountManager instance.
+ *
+ * @param connection a connection to a XMPP server.
+ */
+ public AccountManager(XMPPConnection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Returns true if the server supports creating new accounts. Many servers require
+ * that you not be currently authenticated when creating new accounts, so the safest
+ * behavior is to only create new accounts before having logged in to a server.
+ *
+ * @return true if the server support creating new accounts.
+ */
+ public boolean supportsAccountCreation() {
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ }
+ return info.getType() != IQ.Type.ERROR;
+ }
+ catch (XMPPException xe) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns an Iterator for the (String) names of the required account attributes.
+ * All attributes must be set when creating new accounts. The standard
+ * attributes are as follows: <ul>
+ * <li>name -- the user's name.
+ * <li>first -- the user's first name.
+ * <li>last -- the user's last name.
+ * <li>email -- the user's email address.
+ * <li>city -- the user's city.
+ * <li>state -- the user's state.
+ * <li>zip -- the user's ZIP code.
+ * <li>phone -- the user's phone number.
+ * <li>url -- the user's website.
+ * <li>date -- the date the registration took place.
+ * <li>misc -- other miscellaneous information to associate with the account.
+ * <li>text -- textual information to associate with the account.
+ * <li>remove -- empty flag to remove account.
+ * </ul><p>
+ *
+ * Typically, servers require no attributes when creating new accounts, or just
+ * the user's email address.
+ *
+ * @return the required account attributes.
+ */
+ public Iterator getAccountAttributes() {
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ }
+ Map attributes = info.getAttributes();
+ if (attributes != null) {
+ return attributes.keySet().iterator();
+ }
+ }
+ catch (XMPPException xe) { }
+ return Collections.EMPTY_LIST.iterator();
+ }
+
+ /**
+ * Returns the value of a given account attribute or <tt>null</tt> if the account
+ * attribute wasn't found.
+ *
+ * @param name the name of the account attribute to return its value.
+ * @return the value of the account attribute or <tt>null</tt> if an account
+ * attribute wasn't found for the requested name.
+ */
+ public String getAccountAttribute(String name) {
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ }
+ return (String) info.getAttributes().get(name);
+ }
+ catch (XMPPException xe) { }
+ return null;
+ }
+
+ /**
+ * Returns the instructions for creating a new account, or <tt>null</tt> if there
+ * are no instructions. If present, instructions should be displayed to the end-user
+ * that will complete the registration process.
+ *
+ * @return the account creation instructions, or <tt>null</tt> if there are none.
+ */
+ public String getAccountInstructions() {
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ }
+ return info.getInstructions();
+ }
+ catch (XMPPException xe) {
+ return null;
+ }
+ }
+
+ /**
+ * Creates a new account using the specified username and password. The server may
+ * require a number of extra account attributes such as an email address and phone
+ * number. In that case, Smack will attempt to automatically set all required
+ * attributes with blank values, which may or may not be accepted by the server.
+ * Therefore, it's recommended to check the required account attributes and to let
+ * the end-user populate them with real values instead.
+ *
+ * @param username the username.
+ * @param password the password.
+ * @throws XMPPException if an error occurs creating the account.
+ */
+ public void createAccount(String username, String password) throws XMPPException {
+ if (!supportsAccountCreation()) {
+ throw new XMPPException("Server does not support account creation.");
+ }
+ // Create a map for all the required attributes, but give them blank values.
+ Map attributes = new HashMap();
+ for (Iterator i=getAccountAttributes(); i.hasNext(); ) {
+ String attributeName = (String)i.next();
+ attributes.put(attributeName, "");
+ }
+ createAccount(username, password, attributes);
+ }
+
+ /**
+ * Creates a new account using the specified username, password and account attributes.
+ * The attributes Map must contain only String name/value pairs and must also have values
+ * for all required attributes.
+ *
+ * @param username the username.
+ * @param password the password.
+ * @param attributes the account attributes.
+ * @throws XMPPException if an error occurs creating the account.
+ * @see #getAccountAttributes()
+ */
+ public void createAccount(String username, String password, Map attributes)
+ throws XMPPException
+ {
+ if (!supportsAccountCreation()) {
+ throw new XMPPException("Server does not support account creation.");
+ }
+ Registration reg = new Registration();
+ reg.setType(IQ.Type.SET);
+ reg.setTo(connection.getServiceName());
+ attributes.put("username",username);
+ attributes.put("password",password);
+ reg.setAttributes(attributes);
+ PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+ new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ }
+
+ /**
+ * Changes the password of the currently logged-in account. This operation can only
+ * be performed after a successful login operation has been completed. Not all servers
+ * support changing passwords; an XMPPException will be thrown when that is the case.
+ *
+ * @throws IllegalStateException if not currently logged-in to the server.
+ * @throws XMPPException if an error occurs when changing the password.
+ */
+ public void changePassword(String newPassword) throws XMPPException {
+ Registration reg = new Registration();
+ reg.setType(IQ.Type.SET);
+ reg.setTo(connection.getServiceName());
+ HashMap map = new HashMap();
+ map.put("username",StringUtils.parseName(connection.getUser()));
+ map.put("password",newPassword);
+ reg.setAttributes(map);
+ PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+ new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ }
+
+ /**
+ * Deletes the currently logged-in account from the server. This operation can only
+ * be performed after a successful login operation has been completed. Not all servers
+ * support deleting accounts; an XMPPException will be thrown when that is the case.
+ *
+ * @throws IllegalStateException if not currently logged-in to the server.
+ * @throws XMPPException if an error occurs when deleting the account.
+ */
+ public void deleteAccount() throws XMPPException {
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Must be logged in to delete a account.");
+ }
+ Registration reg = new Registration();
+ reg.setType(IQ.Type.SET);
+ reg.setTo(connection.getServiceName());
+ Map attributes = new HashMap();
+ // To delete an account, we add a single attribute, "remove", that is blank.
+ attributes.put("remove", "");
+ reg.setAttributes(attributes);
+ PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+ new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ }
+
+ /**
+ * Gets the account registration info from the server.
+ *
+ * @throws XMPPException if an error occurs.
+ */
+ private synchronized void getRegistrationInfo() throws XMPPException {
+ Registration reg = new Registration();
+ reg.setTo(connection.getServiceName());
+ PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+ new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ else {
+ info = (Registration)result;
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Chat.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Chat.java
new file mode 100644
index 000000000..57e1633e4
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Chat.java
@@ -0,0 +1,270 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.filter.*;
+
+import java.util.*;
+import java.lang.ref.WeakReference;
+
+/**
+ * A chat is a series of messages sent between two users. Each chat has a unique
+ * thread ID, which is used to track which messages are part of a particular
+ * conversation. Some messages are sent without a thread ID, and some clients
+ * don't send thread IDs at all. Therefore, if a message without a thread ID
+ * arrives it is routed to the most recently created Chat with the message
+ * sender.
+ *
+ * @see XMPPConnection#createChat(String)
+ * @author Matt Tucker
+ */
+public class Chat {
+
+ /**
+ * A prefix helps to make sure that ID's are unique across mutliple instances.
+ */
+ private static String prefix = StringUtils.randomString(5);
+
+ /**
+ * Keeps track of the current increment, which is appended to the prefix to
+ * forum a unique ID.
+ */
+ private static long id = 0;
+
+ /**
+ * Returns the next unique id. Each id made up of a short alphanumeric
+ * prefix along with a unique numeric value.
+ *
+ * @return the next id.
+ */
+ private static synchronized String nextID() {
+ return prefix + Long.toString(id++);
+ }
+
+ private XMPPConnection connection;
+ private String threadID;
+ private String participant;
+ private PacketFilter messageFilter;
+ private PacketCollector messageCollector;
+ private Set listeners = new HashSet();
+
+ /**
+ * Creates a new chat with the specified user.
+ *
+ * @param connection the connection the chat will use.
+ * @param participant the user to chat with.
+ */
+ public Chat(XMPPConnection connection, String participant) {
+ // Automatically assign the next chat ID.
+ this(connection, participant, nextID());
+ }
+
+ /**
+ * Creates a new chat with the specified user and thread ID.
+ *
+ * @param connection the connection the chat will use.
+ * @param participant the user to chat with.
+ * @param threadID the thread ID to use.
+ */
+ public Chat(XMPPConnection connection, String participant, String threadID) {
+ this.connection = connection;
+ this.participant = participant;
+ this.threadID = threadID;
+
+ // Register with the map of chats so that messages with no thread ID
+ // set will be delivered to this Chat.
+ connection.chats.put(StringUtils.parseBareAddress(participant),
+ new WeakReference(this));
+
+ // Filter the messages whose thread equals Chat's id
+ messageFilter = new ThreadFilter(threadID);
+
+ messageCollector = connection.createPacketCollector(messageFilter);
+ }
+
+ /**
+ * Returns the thread id associated with this chat, which corresponds to the
+ * <tt>thread</tt> field of XMPP messages. This method may return <tt>null</tt>
+ * if there is no thread ID is associated with this Chat.
+ *
+ * @return the thread ID of this chat.
+ */
+ public String getThreadID() {
+ return threadID;
+ }
+
+ /**
+ * Returns the name of the user the chat is with.
+ *
+ * @return the name of the user the chat is occuring with.
+ */
+ public String getParticipant() {
+ return participant;
+ }
+
+ /**
+ * Sends the specified text as a message to the other chat participant.
+ * This is a convenience method for:
+ *
+ * <pre>
+ * Message message = chat.createMessage();
+ * message.setBody(messageText);
+ * chat.sendMessage(message);
+ * </pre>
+ *
+ * @param text the text to send.
+ * @throws XMPPException if sending the message fails.
+ */
+ public void sendMessage(String text) throws XMPPException {
+ Message message = createMessage();
+ message.setBody(text);
+ connection.sendPacket(message);
+ }
+
+ /**
+ * Creates a new Message to the chat participant. The message returned
+ * will have its thread property set with this chat ID.
+ *
+ * @return a new message addressed to the chat participant and
+ * using the correct thread value.
+ * @see #sendMessage(Message)
+ */
+ public Message createMessage() {
+ Message message = new Message(participant, Message.Type.CHAT);
+ message.setThread(threadID);
+ return message;
+ }
+
+ /**
+ * Sends a message to the other chat participant. The thread ID, recipient,
+ * and message type of the message will automatically set to those of this chat
+ * in case the Message was not created using the {@link #createMessage() createMessage}
+ * method.
+ *
+ * @param message the message to send.
+ * @throws XMPPException if an error occurs sending the message.
+ */
+ public void sendMessage(Message message) throws XMPPException {
+ // Force the recipient, message type, and thread ID since the user elected
+ // to send the message through this chat object.
+ message.setTo(participant);
+ message.setType(Message.Type.CHAT);
+ message.setThread(threadID);
+ connection.sendPacket(message);
+ }
+
+ /**
+ * Polls for and returns the next message, or <tt>null</tt> if there isn't
+ * a message immediately available. This method provides significantly different
+ * functionalty than the {@link #nextMessage()} method since it's non-blocking.
+ * In other words, the method call will always return immediately, whereas the
+ * nextMessage method will return only when a message is available (or after
+ * a specific timeout).
+ *
+ * @return the next message if one is immediately available and
+ * <tt>null</tt> otherwise.
+ */
+ public Message pollMessage() {
+ return (Message)messageCollector.pollResult();
+ }
+
+ /**
+ * Returns the next available message in the chat. The method call will block
+ * (not return) until a message is available.
+ *
+ * @return the next message.
+ */
+ public Message nextMessage() {
+ return (Message)messageCollector.nextResult();
+ }
+
+ /**
+ * Returns the next available message in the chat. The method call will block
+ * (not return) until a packet is available or the <tt>timeout</tt> has elapased.
+ * If the timeout elapses without a result, <tt>null</tt> will be returned.
+ *
+ * @param timeout the maximum amount of time to wait for the next message.
+ * @return the next message, or <tt>null</tt> if the timeout elapses without a
+ * message becoming available.
+ */
+ public Message nextMessage(long timeout) {
+ return (Message)messageCollector.nextResult(timeout);
+ }
+
+ /**
+ * Adds a packet listener that will be notified of any new messages in the
+ * chat.
+ *
+ * @param listener a packet listener.
+ */
+ public void addMessageListener(PacketListener listener) {
+ connection.addPacketListener(listener, messageFilter);
+ // Keep track of the listener so that we can manually deliver extra
+ // messages to it later if needed.
+ synchronized (listeners) {
+ listeners.add(new WeakReference(listener));
+ }
+ }
+
+ /**
+ * Delivers a message directly to this chat, which will add the message
+ * to the collector and deliver it to all listeners registered with the
+ * Chat. This is used by the XMPPConnection class to deliver messages
+ * without a thread ID.
+ *
+ * @param message the message.
+ */
+ void deliver(Message message) {
+ // Because the collector and listeners are expecting a thread ID with
+ // a specific value, set the thread ID on the message even though it
+ // probably never had one.
+ message.setThread(threadID);
+
+ messageCollector.processPacket(message);
+ synchronized (listeners) {
+ for (Iterator i=listeners.iterator(); i.hasNext(); ) {
+ WeakReference listenerRef = (WeakReference)i.next();
+ PacketListener listener;
+ if ((listener = (PacketListener)listenerRef.get()) != null) {
+ listener.processPacket(message);
+ }
+ // If the reference was cleared, remove it from the set.
+ else {
+ i.remove();
+ }
+ }
+ }
+ }
+
+ public void finalize() throws Throwable {
+ super.finalize();
+ try {
+ if (messageCollector != null) {
+ messageCollector.cancel();
+ }
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionConfiguration.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionConfiguration.java
new file mode 100644
index 000000000..63b6127c3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionConfiguration.java
@@ -0,0 +1,357 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 java.io.File;
+
+/**
+ * Configuration to use while establishing the connection to the server. It is possible to
+ * configure the path to the trustore file that keeps the trusted CA root certificates and
+ * enable or disable all or some of the checkings done while verifying server certificates.<p>
+ *
+ * It is also possible to configure it TLs, SASL or compression are going to be used or not.
+ *
+ * @author Gaston Dombiak
+ */
+public class ConnectionConfiguration implements Cloneable {
+
+ private String serviceName;
+
+ private String host;
+ private int port;
+
+ private String truststorePath;
+ private String truststoreType;
+ private String truststorePassword;
+ private boolean tlsEnabled = true;
+ private boolean verifyChainEnabled = false;
+ private boolean verifyRootCAEnabled = false;
+ private boolean selfSignedCertificateEnabled = false;
+ private boolean expiredCertificatesCheckEnabled = false;
+ private boolean notMatchingDomainCheckEnabled = false;
+
+ private boolean compressionEnabled = false;
+
+ private boolean saslAuthenticationEnabled = true;
+
+ private boolean debuggerEnabled = XMPPConnection.DEBUG_ENABLED;
+
+ public ConnectionConfiguration(String host, int port, String serviceName) {
+ this.host = host;
+ this.port = port;
+ this.serviceName = serviceName;
+
+ // Build the default path to the cacert truststore file. By default we are
+ // going to use the file located in $JREHOME/lib/security/cacerts.
+ String javaHome = System.getProperty("java.home");
+ StringBuffer buffer = new StringBuffer();
+ buffer.append(javaHome).append(File.separator).append("lib");
+ buffer.append(File.separator).append("security");
+ buffer.append(File.separator).append("cacerts");
+ truststorePath = buffer.toString();
+ // Set the default store type
+ truststoreType = "jks";
+ // Set the default password of the cacert file that is "changeit"
+ truststorePassword = "changeit";
+ }
+
+ public ConnectionConfiguration(String host, int port) {
+ this(host, port, host);
+ }
+
+ /**
+ * Returns the server name of the target server.
+ *
+ * @return the server name of the target server.
+ */
+ public String getServiceName() {
+ return serviceName;
+ }
+
+ /**
+ * Returns the host to use when establishing the connection. The host and port to use
+ * might have been resolved by a DNS lookup as specified by the XMPP spec.
+ *
+ * @return the host to use when establishing the connection.
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * Returns the port to use when establishing the connection. The host and port to use
+ * might have been resolved by a DNS lookup as specified by the XMPP spec.
+ *
+ * @return the port to use when establishing the connection.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Returns true if the client is going to try to secure the connection using TLS after
+ * the connection has been established.
+ *
+ * @return true if the client is going to try to secure the connection using TLS after
+ * the connection has been established.
+ */
+ public boolean isTLSEnabled() {
+ return tlsEnabled;
+ }
+
+ /**
+ * Sets if the client is going to try to secure the connection using TLS after
+ * the connection has been established.
+ *
+ * @param tlsEnabled if the client is going to try to secure the connection using TLS after
+ * the connection has been established.
+ */
+ public void setTLSEnabled(boolean tlsEnabled) {
+ this.tlsEnabled = tlsEnabled;
+ }
+
+ /**
+ * Retuns the path to the truststore file. The truststore file contains the root
+ * certificates of several well?known CAs. By default Smack is going to use
+ * the file located in $JREHOME/lib/security/cacerts.
+ *
+ * @return the path to the truststore file.
+ */
+ public String getTruststorePath() {
+ return truststorePath;
+ }
+
+ /**
+ * Sets the path to the truststore file. The truststore file contains the root
+ * certificates of several well?known CAs. By default Smack is going to use
+ * the file located in $JREHOME/lib/security/cacerts.
+ *
+ * @param truststorePath the path to the truststore file.
+ */
+ public void setTruststorePath(String truststorePath) {
+ this.truststorePath = truststorePath;
+ }
+
+ public String getTruststoreType() {
+ return truststoreType;
+ }
+
+ public void setTruststoreType(String truststoreType) {
+ this.truststoreType = truststoreType;
+ }
+
+ /**
+ * Returns the password to use to access the truststore file. It is assumed that all
+ * certificates share the same password of the truststore file.
+ *
+ * @return the password to use to access the truststore file.
+ */
+ public String getTruststorePassword() {
+ return truststorePassword;
+ }
+
+ /**
+ * Sets the password to use to access the truststore file. It is assumed that all
+ * certificates share the same password of the truststore file.
+ *
+ *
+ * @param truststorePassword the password to use to access the truststore file.
+ */
+ public void setTruststorePassword(String truststorePassword) {
+ this.truststorePassword = truststorePassword;
+ }
+
+ /**
+ * Returns true if the whole chain of certificates presented by the server are going to
+ * be checked. By default the certificate chain is not verified.
+ *
+ * @return true if the whole chaing of certificates presented by the server are going to
+ * be checked.
+ */
+ public boolean isVerifyChainEnabled() {
+ return verifyChainEnabled;
+ }
+
+ /**
+ * Sets if the whole chain of certificates presented by the server are going to
+ * be checked. By default the certificate chain is not verified.
+ *
+ * @param verifyChainEnabled if the whole chaing of certificates presented by the server
+ * are going to be checked.
+ */
+ public void setVerifyChainEnabled(boolean verifyChainEnabled) {
+ this.verifyChainEnabled = verifyChainEnabled;
+ }
+
+ /**
+ * Returns true if root CA checking is going to be done. By default checking is disabled.
+ *
+ * @return true if root CA checking is going to be done.
+ */
+ public boolean isVerifyRootCAEnabled() {
+ return verifyRootCAEnabled;
+ }
+
+ /**
+ * Sets if root CA checking is going to be done. By default checking is disabled.
+ *
+ * @param verifyRootCAEnabled if root CA checking is going to be done.
+ */
+ public void setVerifyRootCAEnabled(boolean verifyRootCAEnabled) {
+ this.verifyRootCAEnabled = verifyRootCAEnabled;
+ }
+
+ /**
+ * Returns true if self-signed certificates are going to be accepted. By default
+ * this option is disabled.
+ *
+ * @return true if self-signed certificates are going to be accepted.
+ */
+ public boolean isSelfSignedCertificateEnabled() {
+ return selfSignedCertificateEnabled;
+ }
+
+ /**
+ * Sets if self-signed certificates are going to be accepted. By default
+ * this option is disabled.
+ *
+ * @param selfSignedCertificateEnabled if self-signed certificates are going to be accepted.
+ */
+ public void setSelfSignedCertificateEnabled(boolean selfSignedCertificateEnabled) {
+ this.selfSignedCertificateEnabled = selfSignedCertificateEnabled;
+ }
+
+ /**
+ * Returns true if certificates presented by the server are going to be checked for their
+ * validity. By default certificates are not verified.
+ *
+ * @return true if certificates presented by the server are going to be checked for their
+ * validity.
+ */
+ public boolean isExpiredCertificatesCheckEnabled() {
+ return expiredCertificatesCheckEnabled;
+ }
+
+ /**
+ * Sets if certificates presented by the server are going to be checked for their
+ * validity. By default certificates are not verified.
+ *
+ * @param expiredCertificatesCheckEnabled if certificates presented by the server are going
+ * to be checked for their validity.
+ */
+ public void setExpiredCertificatesCheckEnabled(boolean expiredCertificatesCheckEnabled) {
+ this.expiredCertificatesCheckEnabled = expiredCertificatesCheckEnabled;
+ }
+
+ /**
+ * Returns true if certificates presented by the server are going to be checked for their
+ * domain. By default certificates are not verified.
+ *
+ * @return true if certificates presented by the server are going to be checked for their
+ * domain.
+ */
+ public boolean isNotMatchingDomainCheckEnabled() {
+ return notMatchingDomainCheckEnabled;
+ }
+
+ /**
+ * Sets if certificates presented by the server are going to be checked for their
+ * domain. By default certificates are not verified.
+ *
+ * @param notMatchingDomainCheckEnabled if certificates presented by the server are going
+ * to be checked for their domain.
+ */
+ public void setNotMatchingDomainCheckEnabled(boolean notMatchingDomainCheckEnabled) {
+ this.notMatchingDomainCheckEnabled = notMatchingDomainCheckEnabled;
+ }
+
+ /**
+ * Returns true if the connection is going to use stream compression. Stream compression
+ * will be requested after TLS was established (if TLS was enabled) and only if the server
+ * offered stream compression. With stream compression network traffic can be reduced
+ * up to 90%. By default compression is disabled.
+ *
+ * @return true if the connection is going to use stream compression.
+ */
+ public boolean isCompressionEnabled() {
+ return compressionEnabled;
+ }
+
+ /**
+ * Sets if the connection is going to use stream compression. Stream compression
+ * will be requested after TLS was established (if TLS was enabled) and only if the server
+ * offered stream compression. With stream compression network traffic can be reduced
+ * up to 90%. By default compression is disabled.
+ *
+ * @param compressionEnabled if the connection is going to use stream compression.
+ */
+ public void setCompressionEnabled(boolean compressionEnabled) {
+ this.compressionEnabled = compressionEnabled;
+ }
+
+ /**
+ * Returns true if the client is going to use SASL authentication when logging into the
+ * server. If SASL authenticatin fails then the client will try to use non-sasl authentication.
+ * By default SASL is enabled.
+ *
+ * @return true if the client is going to use SASL authentication when logging into the
+ * server.
+ */
+ public boolean isSASLAuthenticationEnabled() {
+ return saslAuthenticationEnabled;
+ }
+
+ /**
+ * Sets if the client is going to use SASL authentication when logging into the
+ * server. If SASL authenticatin fails then the client will try to use non-sasl authentication.
+ * By default SASL is enabled.
+ *
+ * @param saslAuthenticationEnabled if the client is going to use SASL authentication when
+ * logging into the server.
+ */
+ public void setSASLAuthenticationEnabled(boolean saslAuthenticationEnabled) {
+ this.saslAuthenticationEnabled = saslAuthenticationEnabled;
+ }
+
+ /**
+ * Returns true if the new connection about to be establish is going to be debugged. By
+ * default the value of {@link XMPPConnection#DEBUG_ENABLED} is used.
+ *
+ * @return true if the new connection about to be establish is going to be debugged.
+ */
+ public boolean isDebuggerEnabled() {
+ return debuggerEnabled;
+ }
+
+ /**
+ * Sets if the new connection about to be establish is going to be debugged. By
+ * default the value of {@link XMPPConnection#DEBUG_ENABLED} is used.
+ *
+ * @param debuggerEnabled if the new connection about to be establish is going to be debugged.
+ */
+ public void setDebuggerEnabled(boolean debuggerEnabled) {
+ this.debuggerEnabled = debuggerEnabled;
+ }
+
+ protected Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionEstablishedListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionEstablishedListener.java
new file mode 100644
index 000000000..316615bfc
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionEstablishedListener.java
@@ -0,0 +1,41 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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;
+
+/**
+ * Interface that allows for implementing classes to listen for connection established
+ * events. Listeners are registered with the XMPPConnection class.
+ *
+ * @see XMPPConnection#addConnectionListener
+ * @see XMPPConnection#removeConnectionListener
+ *
+ * @author Gaston Dombiak
+ */
+public interface ConnectionEstablishedListener {
+
+ /**
+ * Notification that a new connection has been established.
+ *
+ * @param connection the new established connection
+ */
+ public void connectionEstablished(XMPPConnection connection);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener.java
new file mode 100644
index 000000000..69bc854f3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener.java
@@ -0,0 +1,45 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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;
+
+/**
+ * Interface that allows for implementing classes to listen for connection closing
+ * events. Listeners are registered with XMPPConnection objects.
+ *
+ * @see XMPPConnection#addConnectionListener
+ * @see XMPPConnection#removeConnectionListener
+ *
+ * @author Matt Tucker
+ */
+public interface ConnectionListener {
+
+ /**
+ * Notification that the connection was closed normally.
+ */
+ public void connectionClosed();
+
+ /**
+ * Notification that the connection was closed due to an exception.
+ *
+ * @param e the exception.
+ */
+ public void connectionClosedOnError(Exception e);
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener2.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener2.java
new file mode 100644
index 000000000..8a3a73174
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener2.java
@@ -0,0 +1,10 @@
+package org.jivesoftware.smack;
+
+public interface ConnectionListener2 {
+
+ /**
+ * Notification that the connection has been authenticated.
+ */
+ public void connectionAuthenticated();
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GoogleTalkConnection.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GoogleTalkConnection.java
new file mode 100644
index 000000000..d81e90aab
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GoogleTalkConnection.java
@@ -0,0 +1,38 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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;
+
+/**
+ * Convenience class to make it easier to connect to the Google Talk IM service.
+ * You can also use {@link XMPPConnection} to connect to Google Talk by specifying
+ * the server name, service name, and port.<p>
+ *
+ * After creating the connection, log in in using a Gmail username and password.
+ * For the Gmail address "jsmith@gmail.com", the username is "jsmith".
+ *
+ * @author Matt Tucker
+ */
+public class GoogleTalkConnection extends XMPPConnection {
+
+ public GoogleTalkConnection() throws XMPPException {
+ super("talk.google.com", 5222, "gmail.com");
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GroupChat.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GroupChat.java
new file mode 100644
index 000000000..668ed8934
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GroupChat.java
@@ -0,0 +1,353 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.filter.*;
+
+import java.util.*;
+
+/**
+ * A GroupChat is a conversation that takes place among many users in a virtual
+ * room. When joining a group chat, you specify a nickname, which is the identity
+ * that other chat room users see.
+ *
+ * @see XMPPConnection#createGroupChat(String)
+ * @author Matt Tucker
+ */
+public class GroupChat {
+
+ private XMPPConnection connection;
+ private String room;
+ private String nickname = null;
+ private boolean joined = false;
+ private List participants = new ArrayList();
+ private List connectionListeners = new ArrayList();
+
+ private PacketFilter presenceFilter;
+ private PacketFilter messageFilter;
+ private PacketCollector messageCollector;
+
+ /**
+ * Creates a new group chat with the specified connection and room name. Note: no
+ * information is sent to or received from the server until you attempt to
+ * {@link #join(String) join} the chat room. On some server implementations,
+ * the room will not be created until the first person joins it.<p>
+ *
+ * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
+ * for the XMPP server example.com). You must ensure that the room address you're
+ * trying to connect to includes the proper chat sub-domain.
+ *
+ * @param connection the XMPP connection.
+ * @param room the name of the room in the form "roomName@service", where
+ * "service" is the hostname at which the multi-user chat
+ * service is running.
+ */
+ public GroupChat(XMPPConnection connection, String room) {
+ this.connection = connection;
+ this.room = room;
+ // Create a collector for all incoming messages.
+ messageFilter = new AndFilter(new FromContainsFilter(room),
+ new PacketTypeFilter(Message.class));
+ messageFilter = new AndFilter(messageFilter, new PacketFilter() {
+ public boolean accept(Packet packet) {
+ Message msg = (Message)packet;
+ return msg.getType() == Message.Type.GROUP_CHAT;
+ }
+ });
+ messageCollector = connection.createPacketCollector(messageFilter);
+ // Create a listener for all presence updates.
+ presenceFilter = new AndFilter(new FromContainsFilter(room),
+ new PacketTypeFilter(Presence.class));
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ Presence presence = (Presence)packet;
+ String from = presence.getFrom();
+ if (presence.getType() == Presence.Type.AVAILABLE) {
+ synchronized (participants) {
+ if (!participants.contains(from)) {
+ participants.add(from);
+ }
+ }
+ }
+ else if (presence.getType() == Presence.Type.UNAVAILABLE) {
+ synchronized (participants) {
+ participants.remove(from);
+ }
+ }
+ }
+ }, presenceFilter);
+ }
+
+ /**
+ * Returns the name of the room this GroupChat object represents.
+ *
+ * @return the groupchat room name.
+ */
+ public String getRoom() {
+ return room;
+ }
+
+ /**
+ * Joins the chat room using the specified nickname. If already joined
+ * using another nickname, this method will first leave the room and then
+ * re-join using the new nickname. The default timeout of 5 seconds for a reply
+ * from the group chat server that the join succeeded will be used.
+ *
+ * @param nickname the nickname to use.
+ * @throws XMPPException if an error occurs joining the room. In particular, a
+ * 409 error can occur if someone is already in the group chat with the same
+ * nickname.
+ */
+ public synchronized void join(String nickname) throws XMPPException {
+ join(nickname, SmackConfiguration.getPacketReplyTimeout());
+ }
+
+ /**
+ * Joins the chat room using the specified nickname. If already joined as
+ * another nickname, will leave as that name first before joining under the new
+ * name.
+ *
+ * @param nickname the nickname to use.
+ * @param timeout the number of milleseconds to wait for a reply from the
+ * group chat that joining the room succeeded.
+ * @throws XMPPException if an error occurs joining the room. In particular, a
+ * 409 error can occur if someone is already in the group chat with the same
+ * nickname.
+ */
+ public synchronized void join(String nickname, long timeout) throws XMPPException {
+ if (nickname == null || nickname.equals("")) {
+ throw new IllegalArgumentException("Nickname must not be null or blank.");
+ }
+ // If we've already joined the room, leave it before joining under a new
+ // nickname.
+ if (joined) {
+ leave();
+ }
+ // We join a room by sending a presence packet where the "to"
+ // field is in the form "roomName@service/nickname"
+ Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
+ joinPresence.setTo(room + "/" + nickname);
+ // Wait for a presence packet back from the server.
+ PacketFilter responseFilter = new AndFilter(
+ new FromContainsFilter(room + "/" + nickname),
+ new PacketTypeFilter(Presence.class));
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send join packet.
+ connection.sendPacket(joinPresence);
+ // Wait up to a certain number of seconds for a reply.
+ Presence presence = (Presence)response.nextResult(timeout);
+ response.cancel();
+ if (presence == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+ this.nickname = nickname;
+ joined = true;
+ }
+
+ /**
+ * Returns true if currently in the group chat (after calling the {@link
+ * #join(String)} method.
+ *
+ * @return true if currently in the group chat room.
+ */
+ public boolean isJoined() {
+ return joined;
+ }
+
+ /**
+ * Leave the chat room.
+ */
+ public synchronized void leave() {
+ // If not joined already, do nothing.
+ if (!joined) {
+ return;
+ }
+ // 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(room + "/" + nickname);
+ connection.sendPacket(leavePresence);
+ // Reset participant information.
+ participants = new ArrayList();
+ nickname = null;
+ joined = false;
+ }
+
+ /**
+ * Returns the nickname that was used to join the room, or <tt>null</tt> if not
+ * currently joined.
+ *
+ * @return the nickname currently being used.
+ */
+ public String getNickname() {
+ return nickname;
+ }
+
+ /**
+ * Returns the number of participants in the group chat.<p>
+ *
+ * Note: this value will only be accurate after joining the group chat, and
+ * may fluctuate over time. If you query this value directly after joining the
+ * group chat it may not be accurate, as it takes a certain amount of time for
+ * the server to send all presence packets to this client.
+ *
+ * @return the number of participants in the group chat.
+ */
+ public int getParticipantCount() {
+ synchronized (participants) {
+ return participants.size();
+ }
+ }
+
+ /**
+ * Returns an Iterator (of Strings) for the list of fully qualified participants
+ * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
+ * Typically, a client would only display the nickname of the participant. To
+ * get the nickname from the fully qualified name, use the
+ * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method.
+ * Note: this value will only be accurate after joining the group chat, and may
+ * fluctuate over time.
+ *
+ * @return an Iterator for the participants in the group chat.
+ */
+ public Iterator getParticipants() {
+ synchronized (participants) {
+ return Collections.unmodifiableList(new ArrayList(participants)).iterator();
+ }
+ }
+
+ /**
+ * Adds a packet listener that will be notified of any new Presence packets
+ * sent to the group chat. Using a listener is a suitable way to know when the list
+ * of participants should be re-loaded due to any changes.
+ *
+ * @param listener a packet listener that will be notified of any presence packets
+ * sent to the group chat.
+ */
+ public void addParticipantListener(PacketListener listener) {
+ connection.addPacketListener(listener, presenceFilter);
+ connectionListeners.add(listener);
+ }
+
+ /**
+ * Sends a message to the chat room.
+ *
+ * @param text the text of the message to send.
+ * @throws XMPPException if sending the message fails.
+ */
+ public void sendMessage(String text) throws XMPPException {
+ Message message = new Message(room, Message.Type.GROUP_CHAT);
+ message.setBody(text);
+ connection.sendPacket(message);
+ }
+
+ /**
+ * Creates a new Message to send to the chat room.
+ *
+ * @return a new Message addressed to the chat room.
+ */
+ public Message createMessage() {
+ return new Message(room, Message.Type.GROUP_CHAT);
+ }
+
+ /**
+ * Sends a Message to the chat room.
+ *
+ * @param message the message.
+ * @throws XMPPException if sending the message fails.
+ */
+ public void sendMessage(Message message) throws XMPPException {
+ connection.sendPacket(message);
+ }
+
+ /**
+ * Polls for and returns the next message, or <tt>null</tt> if there isn't
+ * a message immediately available. This method provides significantly different
+ * functionalty than the {@link #nextMessage()} method since it's non-blocking.
+ * In other words, the method call will always return immediately, whereas the
+ * nextMessage method will return only when a message is available (or after
+ * a specific timeout).
+ *
+ * @return the next message if one is immediately available and
+ * <tt>null</tt> otherwise.
+ */
+ public Message pollMessage() {
+ return (Message)messageCollector.pollResult();
+ }
+
+ /**
+ * Returns the next available message in the chat. The method call will block
+ * (not return) until a message is available.
+ *
+ * @return the next message.
+ */
+ public Message nextMessage() {
+ return (Message)messageCollector.nextResult();
+ }
+
+ /**
+ * Returns the next available message in the chat. The method call will block
+ * (not return) until a packet is available or the <tt>timeout</tt> has elapased.
+ * If the timeout elapses without a result, <tt>null</tt> will be returned.
+ *
+ * @param timeout the maximum amount of time to wait for the next message.
+ * @return the next message, or <tt>null</tt> if the timeout elapses without a
+ * message becoming available.
+ */
+ public Message nextMessage(long timeout) {
+ return (Message)messageCollector.nextResult(timeout);
+ }
+
+ /**
+ * Adds a packet listener that will be notified of any new messages in the
+ * group chat. Only "group chat" messages addressed to this group chat will
+ * be delivered to the listener. If you wish to listen for other packets
+ * that may be associated with this group chat, you should register a
+ * PacketListener directly with the XMPPConnection with the appropriate
+ * PacketListener.
+ *
+ * @param listener a packet listener.
+ */
+ public void addMessageListener(PacketListener listener) {
+ connection.addPacketListener(listener, messageFilter);
+ connectionListeners.add(listener);
+ }
+
+ public void finalize() throws Throwable {
+ super.finalize();
+ try {
+ if (messageCollector != null) {
+ messageCollector.cancel();
+ }
+ // Remove all the PacketListeners added to the connection by this GroupChat
+ for (Iterator it=connectionListeners.iterator(); it.hasNext();) {
+ connection.removePacketListener((PacketListener) it.next());
+ }
+ }
+ catch (Exception e) {}
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/NonSASLAuthentication.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/NonSASLAuthentication.java
new file mode 100644
index 000000000..d392f0509
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/NonSASLAuthentication.java
@@ -0,0 +1,128 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.Authentication;
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * Implementation of JEP-0078: Non-SASL Authentication. Follow the following
+ * <a href=http://www.jabber.org/jeps/jep-0078.html>link</a> to obtain more
+ * information about the JEP.
+ *
+ * @author Gaston Dombiak
+ */
+class NonSASLAuthentication implements UserAuthentication {
+
+ private XMPPConnection connection;
+
+ public NonSASLAuthentication(XMPPConnection connection) {
+ super();
+ this.connection = connection;
+ }
+
+ public String authenticate(String username, String password, String resource) throws
+ XMPPException {
+ // If we send an authentication packet in "get" mode with just the username,
+ // the server will return the list of authentication protocols it supports.
+ Authentication discoveryAuth = new Authentication();
+ discoveryAuth.setType(IQ.Type.GET);
+ discoveryAuth.setUsername(username);
+
+ PacketCollector collector =
+ connection.createPacketCollector(new PacketIDFilter(discoveryAuth.getPacketID()));
+ // Send the packet
+ connection.sendPacket(discoveryAuth);
+ // Wait up to a certain number of seconds for a response from the server.
+ IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ // Otherwise, no error so continue processing.
+ Authentication authTypes = (Authentication) response;
+ collector.cancel();
+
+ // Now, create the authentication packet we'll send to the server.
+ Authentication auth = new Authentication();
+ auth.setUsername(username);
+
+ // Figure out if we should use digest or plain text authentication.
+ if (authTypes.getDigest() != null) {
+ auth.setDigest(connection.getConnectionID(), password);
+ }
+ else if (authTypes.getPassword() != null) {
+ auth.setPassword(password);
+ }
+ else {
+ throw new XMPPException("Server does not support compatible authentication mechanism.");
+ }
+
+ auth.setResource(resource);
+
+ collector = connection.createPacketCollector(new PacketIDFilter(auth.getPacketID()));
+ // Send the packet.
+ connection.sendPacket(auth);
+ // Wait up to a certain number of seconds for a response from the server.
+ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ if (response == null) {
+ throw new XMPPException("Authentication failed.");
+ }
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ // We're done with the collector, so explicitly cancel it.
+ collector.cancel();
+
+ return response.getTo();
+ }
+
+ public String authenticateAnonymously() throws XMPPException {
+ // Create the authentication packet we'll send to the server.
+ Authentication auth = new Authentication();
+
+ PacketCollector collector =
+ connection.createPacketCollector(new PacketIDFilter(auth.getPacketID()));
+ // Send the packet.
+ connection.sendPacket(auth);
+ // Wait up to a certain number of seconds for a response from the server.
+ IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ if (response == null) {
+ throw new XMPPException("Anonymous login failed.");
+ }
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ // We're done with the collector, so explicitly cancel it.
+ collector.cancel();
+
+ if (response.getTo() != null) {
+ return response.getTo();
+ }
+ else {
+ return connection.serviceName + "/" + ((Authentication) response).getResource();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/OpenTrustManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/OpenTrustManager.java
new file mode 100644
index 000000000..3d30a22df
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/OpenTrustManager.java
@@ -0,0 +1,49 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 javax.net.ssl.X509TrustManager;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * Dummy trust manager that trust all certificates presented by the server. This class
+ * is used during old SSL connections.
+ *
+ * @author Gaston Dombiak
+ */
+class OpenTrustManager implements X509TrustManager {
+
+ public OpenTrustManager() {
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ public void checkClientTrusted(X509Certificate[] arg0, String arg1)
+ throws CertificateException {
+ }
+
+ public void checkServerTrusted(X509Certificate[] arg0, String arg1)
+ throws CertificateException {
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketCollector.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketCollector.java
new file mode 100644
index 000000000..b54cc82dc
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketCollector.java
@@ -0,0 +1,185 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.filter.PacketFilter;
+
+import java.util.LinkedList;
+
+/**
+ * Provides a mechanism to collect packets into a result queue that pass a
+ * specified filter. The collector lets you perform blocking and polling
+ * operations on the result queue. So, a PacketCollector is more suitable to
+ * use than a {@link PacketListener} when you need to wait for a specific
+ * result.<p>
+ *
+ * Each packet collector will queue up to 2^16 packets for processing before
+ * older packets are automatically dropped.
+ *
+ * @see XMPPConnection#createPacketCollector(PacketFilter)
+ * @author Matt Tucker
+ */
+public class PacketCollector {
+
+ /**
+ * Max number of packets that any one collector can hold. After the max is
+ * reached, older packets will be automatically dropped from the queue as
+ * new packets are added.
+ */
+ private static final int MAX_PACKETS = 65536;
+
+ private PacketFilter packetFilter;
+ private LinkedList resultQueue;
+ private PacketReader packetReader;
+ private boolean cancelled = false;
+
+ /**
+ * Creates a new packet collector. If the packet filter is <tt>null</tt>, then
+ * all packets will match this collector.
+ *
+ * @param packetReader the packetReader the collector is tied to.
+ * @param packetFilter determines which packets will be returned by this collector.
+ */
+ protected PacketCollector(PacketReader packetReader, PacketFilter packetFilter) {
+ this.packetReader = packetReader;
+ this.packetFilter = packetFilter;
+ this.resultQueue = new LinkedList();
+ // Add the collector to the packet reader's list of active collector.
+ synchronized (packetReader.collectors) {
+ packetReader.collectors.add(this);
+ }
+ }
+
+ /**
+ * Explicitly cancels the packet collector so that no more results are
+ * queued up. Once a packet collector has been cancelled, it cannot be
+ * re-enabled. Instead, a new packet collector must be created.
+ */
+ public void cancel() {
+ // If the packet collector has already been cancelled, do nothing.
+ if (!cancelled) {
+ cancelled = true;
+ // Remove object from collectors list by setting the value in the
+ // list at the correct index to null. The collector thread will
+ // automatically remove the actual list entry when it can.
+ synchronized (packetReader.collectors) {
+ int index = packetReader.collectors.indexOf(this);
+ packetReader.collectors.set(index, null);
+ }
+ }
+ }
+
+ /**
+ * Returns the packet filter associated with this packet collector. The packet
+ * filter is used to determine what packets are queued as results.
+ *
+ * @return the packet filter.
+ */
+ public PacketFilter getPacketFilter() {
+ return packetFilter;
+ }
+
+ /**
+ * Polls to see if a packet is currently available and returns it, or
+ * immediately returns <tt>null</tt> if no packets are currently in the
+ * result queue.
+ *
+ * @return the next packet result, or <tt>null</tt> if there are no more
+ * results.
+ */
+ public synchronized Packet pollResult() {
+ if (resultQueue.isEmpty()) {
+ return null;
+ }
+ else {
+ return (Packet)resultQueue.removeLast();
+ }
+ }
+
+ /**
+ * Returns the next available packet. The method call will block (not return)
+ * until a packet is available.
+ *
+ * @return the next available packet.
+ */
+ public synchronized Packet nextResult() {
+ // Wait indefinitely until there is a result to return.
+ while (resultQueue.isEmpty()) {
+ try {
+ wait();
+ }
+ catch (InterruptedException ie) {
+ // Ignore.
+ }
+ }
+ return (Packet)resultQueue.removeLast();
+ }
+
+ /**
+ * Returns the next available packet. The method call will block (not return)
+ * until a packet is available or the <tt>timeout</tt> has elapased. If the
+ * timeout elapses without a result, <tt>null</tt> will be returned.
+ *
+ * @param timeout the amount of time to wait for the next packet (in milleseconds).
+ * @return the next available packet.
+ */
+ public synchronized Packet nextResult(long timeout) {
+ // Wait up to the specified amount of time for a result.
+ if (resultQueue.isEmpty()) {
+ try {
+ wait(timeout);
+ }
+ catch (InterruptedException ie) {
+ // Ignore.
+ }
+ }
+ // If still no result, return null.
+ if (resultQueue.isEmpty()) {
+ return null;
+ }
+ else {
+ return (Packet)resultQueue.removeLast();
+ }
+ }
+
+ /**
+ * Processes a packet to see if it meets the criteria for this packet collector.
+ * If so, the packet is added to the result queue.
+ *
+ * @param packet the packet to process.
+ */
+ protected synchronized void processPacket(Packet packet) {
+ if (packet == null) {
+ return;
+ }
+ if (packetFilter == null || packetFilter.accept(packet)) {
+ // If the max number of packets has been reached, remove the oldest one.
+ if (resultQueue.size() == MAX_PACKETS) {
+ resultQueue.removeLast();
+ }
+ // Add the new packet.
+ resultQueue.addFirst(packet);
+ // Notify waiting threads a result is available.
+ notifyAll();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketInterceptor.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketInterceptor.java
new file mode 100644
index 000000000..849c3fb38
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketInterceptor.java
@@ -0,0 +1,49 @@
+/**
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2005 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Provides a mechanism to intercept and modify packets that are going to be
+ * sent to the server. PacketInterceptors are added to the {@link XMPPConnection}
+ * together with a {@link org.jivesoftware.smack.filter.PacketFilter} so that only
+ * certain packets are intercepted and processed by the interceptor.<p>
+ *
+ * This allows event-style programming -- every time a new packet is found,
+ * the {@link #interceptPacket(Packet)} method will be called.
+ *
+ * @see XMPPConnection#addPacketWriterInterceptor(PacketInterceptor, org.jivesoftware.smack.filter.PacketFilter)
+ * @author Gaston Dombiak
+ */
+public interface PacketInterceptor {
+
+ /**
+ * Process the packet that is about to be sent to the server. The intercepted
+ * packet can be modified by the interceptor.<p>
+ *
+ * Interceptors are invoked using the same thread that requested the packet
+ * to be sent, so it's very important that implementations of this method
+ * not block for any extended period of time.
+ *
+ * @param packet the packet to is going to be sent to the server.
+ */
+ public void interceptPacket(Packet packet);
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketListener.java
new file mode 100644
index 000000000..77c6deabe
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketListener.java
@@ -0,0 +1,48 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Provides a mechanism to listen for packets that pass a specified filter.
+ * This allows event-style programming -- every time a new packet is found,
+ * the {@link #processPacket(Packet)} method will be called. This is the
+ * opposite approach to the functionality provided by a {@link PacketCollector}
+ * which lets you block while waiting for results.
+ *
+ * @see XMPPConnection#addPacketListener(PacketListener, org.jivesoftware.smack.filter.PacketFilter)
+ * @author Matt Tucker
+ */
+public interface PacketListener {
+
+ /**
+ * Process the next packet sent to this packet listener.<p>
+ *
+ * A single thread is responsible for invoking all listeners, so
+ * it's very important that implementations of this method not block
+ * for any extended period of time.
+ *
+ * @param packet the packet to process.
+ */
+ public void processPacket(Packet packet);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketReader.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketReader.java
new file mode 100644
index 000000000..946468262
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketReader.java
@@ -0,0 +1,854 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.provider.ProviderManager;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.xmlpull.mxp1.MXParser;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Listens for XML traffic from the XMPP server and parses it into packet objects.
+ * The packet reader also manages all packet listeners and collectors.<p>
+ *
+ * @see PacketCollector
+ * @see PacketListener
+ * @author Matt Tucker
+ */
+class PacketReader {
+
+ private Thread readerThread;
+ private Thread listenerThread;
+
+ private XMPPConnection connection;
+ private XmlPullParser parser;
+ private boolean done = false;
+ protected List collectors = new ArrayList();
+ private List listeners = new ArrayList();
+ protected List connectionListeners = new ArrayList();
+
+ private String connectionID = null;
+ private Object connectionIDLock = new Object();
+
+ protected PacketReader(XMPPConnection connection) {
+ this.connection = connection;
+
+ readerThread = new Thread() {
+ public void run() {
+ parsePackets();
+ }
+ };
+ readerThread.setName("Smack Packet Reader");
+ readerThread.setDaemon(true);
+
+ listenerThread = new Thread() {
+ public void run() {
+ try {
+ processListeners();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ };
+ listenerThread.setName("Smack Listener Processor");
+ listenerThread.setDaemon(true);
+
+ resetParser();
+ }
+
+ /**
+ * Creates a new packet collector for this reader. A packet filter determines
+ * which packets will be accumulated by the collector.
+ *
+ * @param packetFilter the packet filter to use.
+ * @return a new packet collector.
+ */
+ public PacketCollector createPacketCollector(PacketFilter packetFilter) {
+ return new PacketCollector(this, packetFilter);
+ }
+
+ /**
+ * Registers a packet listener with this reader. A packet filter determines
+ * which packets will be delivered to the listener.
+ *
+ * @param packetListener the packet listener to notify of new packets.
+ * @param packetFilter the packet filter to use.
+ */
+ public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) {
+ ListenerWrapper wrapper = new ListenerWrapper(this, packetListener,
+ packetFilter);
+ synchronized (listeners) {
+ listeners.add(wrapper);
+ }
+ }
+
+ /**
+ * Removes a packet listener.
+ *
+ * @param packetListener the packet listener to remove.
+ */
+ public void removePacketListener(PacketListener packetListener) {
+ synchronized (listeners) {
+ for (int i=0; i<listeners.size(); i++) {
+ ListenerWrapper wrapper = (ListenerWrapper)listeners.get(i);
+ if (wrapper != null && wrapper.packetListener.equals(packetListener)) {
+ listeners.set(i, null);
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts the packet reader thread and returns once a connection to the server
+ * has been established. A connection will be attempted for a maximum of five
+ * seconds. An XMPPException will be thrown if the connection fails.
+ *
+ * @throws XMPPException if the server fails to send an opening stream back
+ * for more than five seconds.
+ */
+ public void startup() throws XMPPException {
+ readerThread.start();
+ listenerThread.start();
+ // Wait for stream tag before returing. We'll wait a couple of seconds before
+ // giving up and throwing an error.
+ try {
+ synchronized(connectionIDLock) {
+ if (connectionID == null) {
+ // A waiting thread may be woken up before the wait time or a notify
+ // (although this is a rare thing). Therefore, we continue waiting
+ // until either a connectionID has been set (and hence a notify was
+ // made) or the total wait time has elapsed.
+ long waitTime = SmackConfiguration.getPacketReplyTimeout();
+ long start = System.currentTimeMillis();
+ while (connectionID == null && !done) {
+ if (waitTime <= 0) {
+ break;
+ }
+ // Wait 3 times the standard time since TLS may take a while
+ connectionIDLock.wait(waitTime * 3);
+ long now = System.currentTimeMillis();
+ waitTime -= now - start;
+ start = now;
+ }
+ }
+ }
+ }
+ catch (InterruptedException ie) {
+ // Ignore.
+ }
+ if (connectionID == null) {
+ throw new XMPPException("Connection failed. No response from server.");
+ }
+ else {
+ connection.connectionID = connectionID;
+ }
+ }
+
+ /**
+ * Shuts the packet reader down.
+ */
+ public void shutdown() {
+ // Notify connection listeners of the connection closing if done hasn't already been set.
+ if (!done) {
+ ArrayList listenersCopy;
+ synchronized (connectionListeners) {
+ // Make a copy since it's possible that a listener will be removed from the list
+ listenersCopy = new ArrayList(connectionListeners);
+ for (Iterator i=listenersCopy.iterator(); i.hasNext(); ) {
+ ConnectionListener listener = (ConnectionListener)i.next();
+ listener.connectionClosed();
+ }
+ }
+ }
+ done = true;
+
+ // Make sure that the listenerThread is awake to shutdown properly
+ synchronized (listenerThread) {
+ listenerThread.notify();
+ }
+ }
+
+ /**
+ * Sends out a notification that there was an error with the connection
+ * and closes the connection.
+ *
+ * @param e the exception that causes the connection close event.
+ */
+ void notifyConnectionError(Exception e) {
+ done = true;
+ connection.close();
+ // Print the stack trace to help catch the problem
+ e.printStackTrace();
+ // Notify connection listeners of the error.
+ ArrayList listenersCopy;
+ synchronized (connectionListeners) {
+ // Make a copy since it's possible that a listener will be removed from the list
+ listenersCopy = new ArrayList(connectionListeners);
+ for (Iterator i=listenersCopy.iterator(); i.hasNext(); ) {
+ ConnectionListener listener = (ConnectionListener)i.next();
+ listener.connectionClosedOnError(e);
+ }
+ }
+ }
+
+ /**
+ * Resets the parser using the latest connection's reader. Reseting the parser is necessary
+ * when the plain connection has been secured or when a new opening stream element is going
+ * to be sent by the server.
+ */
+ private void resetParser() {
+ try {
+ parser = new MXParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ parser.setInput(connection.reader);
+ }
+ catch (XmlPullParserException xppe) {
+ xppe.printStackTrace();
+ }
+ }
+
+ /**
+ * Process listeners.
+ */
+ private void processListeners() {
+ while (!done) {
+ synchronized (listeners) {
+ if (listeners.size() > 0) {
+ for (int i=listeners.size()-1; i>=0; i--) {
+ if (listeners.get(i) == null) {
+ listeners.remove(i);
+ }
+ }
+ }
+ }
+ boolean processedPacket = false;
+ int size = listeners.size();
+ for (int i=0; i<size; i++) {
+ ListenerWrapper wrapper = (ListenerWrapper)listeners.get(i);
+ if (wrapper != null) {
+ processedPacket = processedPacket || wrapper.notifyListener();
+ }
+ }
+ if (!processedPacket) {
+ try {
+ // Wait until more packets are ready to be processed.
+ synchronized (listenerThread) {
+ listenerThread.wait();
+ }
+ }
+ catch (InterruptedException ie) {
+ // Ignore.
+ }
+ }
+ }
+ }
+
+ /**
+ * Parse top-level packets in order to process them further.
+ */
+ private void parsePackets() {
+ try {
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("message")) {
+ processPacket(PacketParserUtils.parseMessage(parser));
+ }
+ else if (parser.getName().equals("iq")) {
+ processPacket(parseIQ(parser));
+ }
+ else if (parser.getName().equals("presence")) {
+ processPacket(PacketParserUtils.parsePresence(parser));
+ }
+ // We found an opening stream. Record information about it, then notify
+ // the connectionID lock so that the packet reader startup can finish.
+ else if (parser.getName().equals("stream")) {
+ // Ensure the correct jabber:client namespace is being used.
+ if ("jabber:client".equals(parser.getNamespace(null))) {
+ // Get the connection id.
+ for (int i=0; i<parser.getAttributeCount(); i++) {
+ if (parser.getAttributeName(i).equals("id")) {
+ // Save the connectionID
+ connectionID = parser.getAttributeValue(i);
+ if (!"1.0".equals(parser.getAttributeValue("", "version"))) {
+ // Notify that a stream has been opened if the
+ // server is not XMPP 1.0 compliant otherwise make the
+ // notification after TLS has been negotiated or if TLS
+ // is not supported
+ releaseConnectionIDLock();
+ }
+ }
+ else if (parser.getAttributeName(i).equals("from")) {
+ // Use the server name that the server says that it is.
+ connection.serviceName = parser.getAttributeValue(i);
+ }
+ }
+ }
+ }
+ else if (parser.getName().equals("error")) {
+ throw new XMPPException(parseStreamError(parser));
+ }
+ else if (parser.getName().equals("features")) {
+ parseFeatures(parser);
+ }
+ else if (parser.getName().equals("proceed")) {
+ // Secure the connection by negotiating TLS
+ connection.proceedTLSReceived();
+ // Reset the state of the parser since a new stream element is going
+ // to be sent by the server
+ resetParser();
+ }
+ else if (parser.getName().equals("failure")) {
+ String namespace = parser.getNamespace(null);
+ if ("urn:ietf:params:xml:ns:xmpp-tls".equals(namespace)) {
+ // TLS negotiation has failed. The server will close the connection
+ throw new Exception("TLS negotiation has failed");
+ }
+ else if ("http://jabber.org/protocol/compress".equals(namespace)) {
+ // Stream compression has been denied. This is a recoverable
+ // situation. It is still possible to authenticate and
+ // use the connection but using an uncompressed connection
+ connection.streamCompressionDenied();
+ }
+ else {
+ // SASL authentication has failed. The server may close the connection
+ // depending on the number of retries
+ connection.getSASLAuthentication().authenticationFailed();
+ }
+ }
+ else if (parser.getName().equals("challenge")) {
+ // The server is challenging the SASL authentication made by the client
+ connection.getSASLAuthentication().challengeReceived(parser.nextText());
+ }
+ else if (parser.getName().equals("success")) {
+ // We now need to bind a resource for the connection
+ // Open a new stream and wait for the response
+ connection.packetWriter.openStream();
+
+ // Reset the state of the parser since a new stream element is going
+ // to be sent by the server
+ resetParser();
+
+ // The SASL authentication with the server was successful. The next step
+ // will be to bind the resource
+ connection.getSASLAuthentication().authenticated();
+ }
+ else if (parser.getName().equals("compressed")) {
+ // Server confirmed that it's possible to use stream compression. Start
+ // stream compression
+ connection.startStreamCompression();
+ // Reset the state of the parser since a new stream element is going
+ // to be sent by the server
+ resetParser();
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("stream")) {
+ // Close the connection.
+ connection.close();
+ }
+ }
+ eventType = parser.next();
+ } while (!done && eventType != XmlPullParser.END_DOCUMENT);
+ }
+ catch (Exception e) {
+ if (!done) {
+ // Close the connection and notify connection listeners of the
+ // error.
+ notifyConnectionError(e);
+ }
+ }
+ }
+
+ /**
+ * Releases the connection ID lock so that the thread that was waiting can resume. The
+ * lock will be released when one of the following three conditions is met:<p>
+ *
+ * 1) An opening stream was sent from a non XMPP 1.0 compliant server
+ * 2) Stream features were received from an XMPP 1.0 compliant server that does not support TLS
+ * 3) TLS negotiation was successful
+ *
+ */
+ private void releaseConnectionIDLock() {
+ synchronized(connectionIDLock) {
+ connectionIDLock.notifyAll();
+ }
+ }
+
+ /**
+ * Processes a packet after it's been fully parsed by looping through the installed
+ * packet collectors and listeners and letting them examine the packet to see if
+ * they are a match with the filter.
+ *
+ * @param packet the packet to process.
+ */
+ private void processPacket(Packet packet) {
+ if (packet == null) {
+ return;
+ }
+
+ // Remove all null values from the collectors list.
+ synchronized (collectors) {
+ for (int i=collectors.size()-1; i>=0; i--) {
+ if (collectors.get(i) == null) {
+ collectors.remove(i);
+ }
+ }
+ }
+
+ // Loop through all collectors and notify the appropriate ones.
+ int size = collectors.size();
+ for (int i=0; i<size; i++) {
+ PacketCollector collector = (PacketCollector)collectors.get(i);
+ if (collector != null) {
+ // Have the collector process the packet to see if it wants to handle it.
+ collector.processPacket(packet);
+ }
+ }
+
+ // Notify the listener thread that packets are waiting.
+ synchronized (listenerThread) {
+ listenerThread.notifyAll();
+ }
+ }
+
+ private StreamError parseStreamError(XmlPullParser parser) throws IOException,
+ XmlPullParserException {
+ StreamError streamError = null;
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ streamError = new StreamError(parser.getName());
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("error")) {
+ done = true;
+ }
+ }
+ }
+ return streamError;
+ }
+
+ private void parseFeatures(XmlPullParser parser) throws Exception {
+ boolean startTLSReceived = false;
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("starttls")) {
+ startTLSReceived = true;
+ // Confirm the server that we want to use TLS
+ connection.startTLSReceived();
+ }
+ else if (parser.getName().equals("mechanisms")) {
+ // The server is reporting available SASL mechanisms. Store this information
+ // which will be used later while logging (i.e. authenticating) into
+ // the server
+ connection.getSASLAuthentication()
+ .setAvailableSASLMethods(parseMechanisms(parser));
+ }
+ else if (parser.getName().equals("bind")) {
+ // The server requires the client to bind a resource to the stream
+ connection.getSASLAuthentication().bindingRequired();
+ }
+ else if (parser.getName().equals("session")) {
+ // The server supports sessions
+ connection.getSASLAuthentication().sessionsSupported();
+ }
+ else if (parser.getName().equals("compression")) {
+ // The server supports stream compression
+ connection.setAvailableCompressionMethods(parseCompressionMethods(parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("features")) {
+ done = true;
+ }
+ }
+ }
+ if (!startTLSReceived) {
+ releaseConnectionIDLock();
+ }
+ }
+ /**
+ * Sends out a notification that there was an error with the connection and
+ * closes the connection.
+ *
+ * @param e
+ * the exception that causes the connection close event.
+ */
+ void notifyConnectionAuthenticated() {
+ ArrayList listenersCopy;
+ synchronized (connectionListeners) {
+ // Make a copy since it's possible that a listener will be removed
+ // from the list
+ listenersCopy = new ArrayList(connectionListeners);
+ for (Iterator i = listenersCopy.iterator(); i.hasNext();) {
+ ConnectionListener listener = (ConnectionListener) i.next();
+ if (listener instanceof ConnectionListener2)
+ ((ConnectionListener2) listener).connectionAuthenticated();
+ }
+ }
+ }
+ /**
+ * Returns a collection of Stings with the mechanisms included in the
+ * mechanisms stanza.
+ *
+ * @param parser
+ * the XML parser, positioned at the start of an IQ packet.
+ * @return a collection of Stings with the mechanisms included in the
+ * mechanisms stanza.
+ * @throws Exception
+ * if an exception occurs while parsing the stanza.
+ */
+ private Collection parseMechanisms(XmlPullParser parser) throws Exception {
+ List mechanisms = new ArrayList();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String elementName = parser.getName();
+ if (elementName.equals("mechanism")) {
+ mechanisms.add(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("mechanisms")) {
+ done = true;
+ }
+ }
+ }
+ return mechanisms;
+ }
+
+ private Collection parseCompressionMethods(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ List methods = new ArrayList();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String elementName = parser.getName();
+ if (elementName.equals("method")) {
+ methods.add(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("compression")) {
+ done = true;
+ }
+ }
+ }
+ return methods;
+ }
+
+ /**
+ * Parses an IQ packet.
+ *
+ * @param parser the XML parser, positioned at the start of an IQ packet.
+ * @return an IQ object.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ private IQ parseIQ(XmlPullParser parser) throws Exception {
+ IQ iqPacket = null;
+
+ String id = parser.getAttributeValue("", "id");
+ String to = parser.getAttributeValue("", "to");
+ String from = parser.getAttributeValue("", "from");
+ IQ.Type type = IQ.Type.fromString(parser.getAttributeValue("", "type"));
+ XMPPError error = null;
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String elementName = parser.getName();
+ String namespace = parser.getNamespace();
+ if (elementName.equals("error")) {
+ error = PacketParserUtils.parseError(parser);
+ }
+ else if (elementName.equals("query") && namespace.equals("jabber:iq:auth")) {
+ iqPacket = parseAuthentication(parser);
+ }
+ else if (elementName.equals("query") && namespace.equals("jabber:iq:roster")) {
+ iqPacket = parseRoster(parser);
+ }
+ else if (elementName.equals("query") && namespace.equals("jabber:iq:register")) {
+ iqPacket = parseRegistration(parser);
+ }
+ else if (elementName.equals("bind") &&
+ namespace.equals("urn:ietf:params:xml:ns:xmpp-bind")) {
+ iqPacket = parseResourceBinding(parser);
+ }
+ // Otherwise, see if there is a registered provider for
+ // this element name and namespace.
+ else {
+ Object provider = ProviderManager.getDefault().getIQProvider(elementName, namespace);
+ if (provider != null) {
+ if (provider instanceof IQProvider) {
+ iqPacket = ((IQProvider)provider).parseIQ(parser);
+ }
+ else if (provider instanceof Class) {
+ iqPacket = (IQ)PacketParserUtils.parseWithIntrospection(elementName,
+ (Class)provider, parser);
+ }
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("iq")) {
+ done = true;
+ }
+ }
+ }
+ // Decide what to do when an IQ packet was not understood
+ if (iqPacket == null) {
+ if (IQ.Type.GET == type || IQ.Type.SET == type ) {
+ // If the IQ stanza is of type "get" or "set" containing a child element
+ // qualified by a namespace it does not understand, then answer an IQ of
+ // type "error" with code 501 ("feature-not-implemented")
+ iqPacket = new IQ() {
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ iqPacket.setPacketID(id);
+ iqPacket.setTo(from);
+ iqPacket.setFrom(to);
+ iqPacket.setType(IQ.Type.ERROR);
+ iqPacket.setError(new XMPPError(501, "feature-not-implemented"));
+ connection.sendPacket(iqPacket);
+ return null;
+ }
+ else {
+ // If an IQ packet wasn't created above, create an empty IQ packet.
+ iqPacket = new IQ() {
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ }
+ }
+
+ // Set basic values on the iq packet.
+ iqPacket.setPacketID(id);
+ iqPacket.setTo(to);
+ iqPacket.setFrom(from);
+ iqPacket.setType(type);
+ iqPacket.setError(error);
+
+ return iqPacket;
+ }
+
+ private Authentication parseAuthentication(XmlPullParser parser) throws Exception {
+ Authentication authentication = new Authentication();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("username")) {
+ authentication.setUsername(parser.nextText());
+ }
+ else if (parser.getName().equals("password")) {
+ authentication.setPassword(parser.nextText());
+ }
+ else if (parser.getName().equals("digest")) {
+ authentication.setDigest(parser.nextText());
+ }
+ else if (parser.getName().equals("resource")) {
+ authentication.setResource(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+ return authentication;
+ }
+
+ private RosterPacket parseRoster(XmlPullParser parser) throws Exception {
+ RosterPacket roster = new RosterPacket();
+ boolean done = false;
+ RosterPacket.Item item = null;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ String jid = parser.getAttributeValue("", "jid");
+ String name = parser.getAttributeValue("", "name");
+ // Create packet.
+ item = new RosterPacket.Item(jid, name);
+ // Set status.
+ String ask = parser.getAttributeValue("", "ask");
+ RosterPacket.ItemStatus status = RosterPacket.ItemStatus.fromString(ask);
+ item.setItemStatus(status);
+ // Set type.
+ String subscription = parser.getAttributeValue("", "subscription");
+ RosterPacket.ItemType type = RosterPacket.ItemType.fromString(subscription);
+ item.setItemType(type);
+ }
+ if (parser.getName().equals("group") && item!= null) {
+ item.addGroupName(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ roster.addRosterItem(item);
+ }
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+ return roster;
+ }
+
+ private Registration parseRegistration(XmlPullParser parser) throws Exception {
+ Registration registration = new Registration();
+ Map fields = null;
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ // Any element that's in the jabber:iq:register namespace,
+ // attempt to parse it if it's in the form <name>value</name>.
+ if (parser.getNamespace().equals("jabber:iq:register")) {
+ String name = parser.getName();
+ String value = "";
+ if (fields == null) {
+ fields = new HashMap();
+ }
+
+ if (parser.next() == XmlPullParser.TEXT) {
+ value = parser.getText();
+ }
+ // Ignore instructions, but anything else should be added to the map.
+ if (!name.equals("instructions")) {
+ fields.put(name, value);
+ }
+ else {
+ registration.setInstructions(value);
+ }
+}
+ // Otherwise, it must be a packet extension.
+ else {
+ registration.addExtension(
+ PacketParserUtils.parsePacketExtension(
+ parser.getName(),
+ parser.getNamespace(),
+ parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+ registration.setAttributes(fields);
+ return registration;
+ }
+
+ private Bind parseResourceBinding(XmlPullParser parser) throws IOException,
+ XmlPullParserException {
+ Bind bind = new Bind();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("resource")) {
+ bind.setResource(parser.nextText());
+ }
+ else if (parser.getName().equals("jid")) {
+ bind.setJid(parser.nextText());
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("bind")) {
+ done = true;
+ }
+ }
+ }
+
+ return bind;
+ }
+
+ /**
+ * A wrapper class to associate a packet collector with a listener.
+ */
+ private static class ListenerWrapper {
+
+ private PacketListener packetListener;
+ private PacketCollector packetCollector;
+
+ public ListenerWrapper(PacketReader packetReader, PacketListener packetListener,
+ PacketFilter packetFilter)
+ {
+ this.packetListener = packetListener;
+ this.packetCollector = new PacketCollector(packetReader, packetFilter);
+ }
+
+ public boolean equals(Object object) {
+ if (object == null) {
+ return false;
+ }
+ if (object instanceof ListenerWrapper) {
+ return ((ListenerWrapper)object).packetListener.equals(this.packetListener);
+ }
+ else if (object instanceof PacketListener) {
+ return object.equals(this.packetListener);
+ }
+ return false;
+ }
+
+ public boolean notifyListener() {
+ Packet packet = packetCollector.pollResult();
+ if (packet != null) {
+ packetListener.processPacket(packet);
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ public void cancel() {
+ packetCollector.cancel();
+ packetCollector = null;
+ packetListener = null;
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketWriter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketWriter.java
new file mode 100644
index 000000000..cbfc6be53
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketWriter.java
@@ -0,0 +1,451 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 java.util.*;
+import java.io.*;
+
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Writes packets to a XMPP server. Packets are sent using a dedicated thread. Packet
+ * interceptors can be registered to dynamically modify packets before they're actually
+ * sent. Packet listeners can be registered to listen for all outgoing packets.
+ *
+ * @author Matt Tucker
+ */
+class PacketWriter {
+
+ private Thread writerThread;
+ private Writer writer;
+ private XMPPConnection connection;
+ private LinkedList queue;
+ private boolean done = false;
+
+ private List listeners = new ArrayList();
+ private boolean listenersDeleted = false;
+
+ /**
+ * List of PacketInterceptor that will be notified when a new packet is about to be
+ * sent to the server. These interceptors may modify the packet before it is being
+ * actually sent to the server.
+ */
+ private List interceptors = new ArrayList();
+ /**
+ * Flag that indicates if an interceptor was deleted. This is an optimization flag.
+ */
+ private boolean interceptorDeleted = false;
+
+ /**
+ * Creates a new packet writer with the specified connection.
+ *
+ * @param connection the connection.
+ */
+ protected PacketWriter(XMPPConnection connection) {
+ this.connection = connection;
+ this.writer = connection.writer;
+ this.queue = new LinkedList();
+
+ writerThread = new Thread() {
+ public void run() {
+ writePackets();
+ }
+ };
+ writerThread.setName("Smack Packet Writer");
+ writerThread.setDaemon(true);
+
+ // Schedule a keep-alive task to run if the feature is enabled. will write
+ // out a space character each time it runs to keep the TCP/IP connection open.
+ int keepAliveInterval = SmackConfiguration.getKeepAliveInterval();
+ if (keepAliveInterval > 0) {
+ Thread keepAliveThread = new Thread(new KeepAliveTask(keepAliveInterval));
+ keepAliveThread.setDaemon(true);
+ keepAliveThread.start();
+ }
+ }
+
+ /**
+ * Sends the specified packet to the server.
+ *
+ * @param packet the packet to send.
+ */
+ public void sendPacket(Packet packet) {
+ if (!done) {
+ // Invoke interceptors for the new packet that is about to be sent. Interceptors
+ // may modify the content of the packet.
+ processInterceptors(packet);
+
+ synchronized(queue) {
+ queue.addFirst(packet);
+ queue.notifyAll();
+ }
+
+ // Process packet writer listeners. Note that we're using the sending
+ // thread so it's expected that listeners are fast.
+ processListeners(packet);
+ }
+ }
+
+ /**
+ * Registers a packet listener with this writer. The listener will be
+ * notified immediately after every packet this writer sends. A packet filter
+ * determines which packets will be delivered to the listener. Note that the thread
+ * that writes packets will be used to invoke the listeners. Therefore, each
+ * packet listener should complete all operations quickly or use a different
+ * thread for processing.
+ *
+ * @param packetListener the packet listener to notify of sent packets.
+ * @param packetFilter the packet filter to use.
+ */
+ public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) {
+ synchronized (listeners) {
+ listeners.add(new ListenerWrapper(packetListener, packetFilter));
+ }
+ }
+
+ /**
+ * Removes a packet listener.
+ *
+ * @param packetListener the packet listener to remove.
+ */
+ public void removePacketListener(PacketListener packetListener) {
+ synchronized (listeners) {
+ for (int i=0; i<listeners.size(); i++) {
+ ListenerWrapper wrapper = (ListenerWrapper)listeners.get(i);
+ if (wrapper != null && wrapper.packetListener.equals(packetListener)) {
+ listeners.set(i, null);
+ // Set the flag to indicate that the listener list needs
+ // to be cleaned up.
+ listenersDeleted = true;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the number of registered packet listeners.
+ *
+ * @return the count of packet listeners.
+ */
+ public int getPacketListenerCount() {
+ synchronized (listeners) {
+ return listeners.size();
+ }
+ }
+
+ /**
+ * Registers a packet interceptor with this writer. The interceptor will be
+ * notified of every packet that this writer is about to send. Interceptors
+ * may modify the packet to be sent. A packet filter determines which packets
+ * will be delivered to the interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to notify of packets about to be sent.
+ * @param packetFilter the packet filter to use.
+ */
+ public void addPacketInterceptor(PacketInterceptor packetInterceptor, PacketFilter packetFilter) {
+ synchronized (interceptors) {
+ interceptors.add(new InterceptorWrapper(packetInterceptor, packetFilter));
+ }
+ }
+
+ /**
+ * Removes a packet interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to remove.
+ */
+ public void removePacketInterceptor(PacketInterceptor packetInterceptor) {
+ synchronized (interceptors) {
+ for (int i=0; i<interceptors.size(); i++) {
+ InterceptorWrapper wrapper = (InterceptorWrapper)interceptors.get(i);
+ if (wrapper != null && wrapper.packetInterceptor.equals(packetInterceptor)) {
+ interceptors.set(i, null);
+ // Set the flag to indicate that the interceptor list needs
+ // to be cleaned up.
+ interceptorDeleted = true;
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts the packet writer thread and opens a connection to the server. The
+ * packet writer will continue writing packets until {@link #shutdown} or an
+ * error occurs.
+ */
+ public void startup() {
+ writerThread.start();
+ }
+
+ void setWriter(Writer writer) {
+ this.writer = writer;
+ }
+
+ /**
+ * Shuts down the packet writer. Once this method has been called, no further
+ * packets will be written to the server.
+ */
+ public void shutdown() {
+ done = true;
+ }
+
+ /**
+ * Returns the next available packet from the queue for writing.
+ *
+ * @return the next packet for writing.
+ */
+ private Packet nextPacket() {
+ synchronized(queue) {
+ while (!done && queue.size() == 0) {
+ try {
+ queue.wait(2000);
+ }
+ catch (InterruptedException ie) { }
+ }
+ if (queue.size() > 0) {
+ return (Packet)queue.removeLast();
+ }
+ else {
+ return null;
+ }
+ }
+ }
+
+ private void writePackets() {
+ try {
+ // Open the stream.
+ openStream();
+ // Write out packets from the queue.
+ while (!done) {
+ Packet packet = nextPacket();
+ if (packet != null) {
+ synchronized (writer) {
+ writer.write(packet.toXML());
+ writer.flush();
+ }
+ }
+ }
+ // Close the stream.
+ try {
+ writer.write("</stream:stream>");
+ writer.flush();
+ }
+ catch (Exception e) { }
+ finally {
+ try {
+ writer.close();
+ }
+ catch (Exception e) { }
+ }
+ }
+ catch (IOException ioe){
+ if (!done) {
+ done = true;
+ connection.packetReader.notifyConnectionError(ioe);
+ }
+ }
+ }
+
+ /**
+ * Process listeners.
+ */
+ private void processListeners(Packet packet) {
+ // Clean up null entries in the listeners list if the flag is set. List
+ // removes are done seperately so that the main notification process doesn't
+ // need to synchronize on the list.
+ synchronized (listeners) {
+ if (listenersDeleted) {
+ for (int i=listeners.size()-1; i>=0; i--) {
+ if (listeners.get(i) == null) {
+ listeners.remove(i);
+ }
+ }
+ listenersDeleted = false;
+ }
+ }
+ // Notify the listeners of the new sent packet
+ int size = listeners.size();
+ for (int i=0; i<size; i++) {
+ ListenerWrapper listenerWrapper = (ListenerWrapper)listeners.get(i);
+ if (listenerWrapper != null) {
+ listenerWrapper.notifyListener(packet);
+ }
+ }
+ }
+
+ /**
+ * Process interceptors. Interceptors may modify the packet that is about to be sent.
+ * Since the thread that requested to send the packet will invoke all interceptors, it
+ * is important that interceptors perform their work as soon as possible so that the
+ * thread does not remain blocked for a long period.
+ *
+ * @param packet the packet that is going to be sent to the server
+ */
+ private void processInterceptors(Packet packet) {
+ if (packet != null) {
+ // Clean up null entries in the interceptors list if the flag is set. List
+ // removes are done seperately so that the main notification process doesn't
+ // need to synchronize on the list.
+ synchronized (interceptors) {
+ if (interceptorDeleted) {
+ for (int i=interceptors.size()-1; i>=0; i--) {
+ if (interceptors.get(i) == null) {
+ interceptors.remove(i);
+ }
+ }
+ interceptorDeleted = false;
+ }
+ }
+ // Notify the interceptors of the new packet to be sent
+ int size = interceptors.size();
+ for (int i=0; i<size; i++) {
+ InterceptorWrapper interceptorWrapper = (InterceptorWrapper)interceptors.get(i);
+ if (interceptorWrapper != null) {
+ interceptorWrapper.notifyListener(packet);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sends to the server a new stream element. This operation may be requested several times
+ * so we need to encapsulate the logic in one place. This message will be sent while doing
+ * TLS, SASL and resource binding.
+ *
+ * @throws IOException If an error occurs while sending the stanza to the server.
+ */
+ void openStream() throws IOException {
+ StringBuffer stream = new StringBuffer();
+ stream.append("<stream:stream");
+ stream.append(" to=\"").append(connection.serviceName).append("\"");
+ stream.append(" xmlns=\"jabber:client\"");
+ stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
+ if (connection instanceof SSLXMPPConnection) {
+ // Old SSL connections should not include indicate XMPP 1.0 compliance
+ stream.append(">");
+ }
+ else {
+ stream.append(" version=\"1.0\">");
+ }
+ writer.write(stream.toString());
+ writer.flush();
+ }
+
+ /**
+ * A wrapper class to associate a packet filter with a listener.
+ */
+ private static class ListenerWrapper {
+
+ private PacketListener packetListener;
+ private PacketFilter packetFilter;
+
+ public ListenerWrapper(PacketListener packetListener,
+ PacketFilter packetFilter)
+ {
+ this.packetListener = packetListener;
+ this.packetFilter = packetFilter;
+ }
+
+ public boolean equals(Object object) {
+ if (object == null) {
+ return false;
+ }
+ if (object instanceof ListenerWrapper) {
+ return ((ListenerWrapper)object).packetListener.equals(this.packetListener);
+ }
+ else if (object instanceof PacketListener) {
+ return object.equals(this.packetListener);
+ }
+ return false;
+ }
+
+ public void notifyListener(Packet packet) {
+ if (packetFilter == null || packetFilter.accept(packet)) {
+ packetListener.processPacket(packet);
+ }
+ }
+ }
+
+ /**
+ * A wrapper class to associate a packet filter with an interceptor.
+ */
+ private static class InterceptorWrapper {
+
+ private PacketInterceptor packetInterceptor;
+ private PacketFilter packetFilter;
+
+ public InterceptorWrapper(PacketInterceptor packetInterceptor, PacketFilter packetFilter)
+ {
+ this.packetInterceptor = packetInterceptor;
+ this.packetFilter = packetFilter;
+ }
+
+ public boolean equals(Object object) {
+ if (object == null) {
+ return false;
+ }
+ if (object instanceof InterceptorWrapper) {
+ return ((InterceptorWrapper) object).packetInterceptor
+ .equals(this.packetInterceptor);
+ }
+ else if (object instanceof PacketInterceptor) {
+ return object.equals(this.packetInterceptor);
+ }
+ return false;
+ }
+
+ public void notifyListener(Packet packet) {
+ if (packetFilter == null || packetFilter.accept(packet)) {
+ packetInterceptor.interceptPacket(packet);
+ }
+ }
+ }
+
+ /**
+ * A TimerTask that keeps connections to the server alive by sending a space
+ * character on an interval.
+ */
+ private class KeepAliveTask implements Runnable {
+
+ private int delay;
+
+ public KeepAliveTask(int delay) {
+ this.delay = delay;
+ }
+
+ public void run() {
+ while (!done) {
+ synchronized (writer) {
+ try {
+ writer.write(" ");
+ writer.flush();
+ }
+ catch (Exception e) { }
+ }
+ try {
+ // Sleep until we should write the next keep-alive.
+ Thread.sleep(delay);
+ }
+ catch (InterruptedException ie) { }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Roster.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Roster.java
new file mode 100644
index 000000000..196a4c3ea
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Roster.java
@@ -0,0 +1,813 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.*;
+
+/**
+ * Represents a user's roster, which is the collection of users a person receives
+ * presence updates for. Roster items are categorized into groups for easier management.<p>
+ *
+ * Others users may attempt to subscribe to this user using a subscription request. Three
+ * modes are supported for handling these requests: <ul>
+ * <li> SUBSCRIPTION_ACCEPT_ALL -- accept all subscription requests.
+ * <li> SUBSCRIPTION_REJECT_ALL -- reject all subscription requests.
+ * <li> SUBSCRIPTION_MANUAL -- manually process all subscription requests. </ul>
+ *
+ * @see XMPPConnection#getRoster()
+ * @author Matt Tucker
+ */
+public class Roster {
+
+ /**
+ * Automatically accept all subscription and unsubscription requests. This is
+ * the default mode and is suitable for simple client. More complex client will
+ * likely wish to handle subscription requests manually.
+ */
+ public static final int SUBSCRIPTION_ACCEPT_ALL = 0;
+
+ /**
+ * Automatically reject all subscription requests.
+ */
+ public static final int SUBSCRIPTION_REJECT_ALL = 1;
+
+ /**
+ * Subscription requests are ignored, which means they must be manually
+ * processed by registering a listener for presence packets and then looking
+ * for any presence requests that have the type Presence.Type.SUBSCRIBE or
+ * Presence.Type.UNSUBSCRIBE.
+ */
+ public static final int SUBSCRIPTION_MANUAL = 2;
+
+ /**
+ * The default subscription processing mode to use when a Roster is created. By default
+ * all subscription requests are automatically accepted.
+ */
+ private static int defaultSubscriptionMode = SUBSCRIPTION_ACCEPT_ALL;
+
+ private XMPPConnection connection;
+ private Map groups;
+ private List entries;
+ private List unfiledEntries;
+ private List rosterListeners;
+ private Map presenceMap;
+ // The roster is marked as initialized when at least a single roster packet
+ // has been recieved and processed.
+ boolean rosterInitialized = false;
+
+ private int subscriptionMode = getDefaultSubscriptionMode();
+
+ /**
+ * Returns the default subscription processing mode to use when a new Roster is created. The
+ * subscription processing mode dictates what action Smack will take when subscription
+ * requests from other users are made. The default subscription mode
+ * is {@link #SUBSCRIPTION_ACCEPT_ALL}.
+ *
+ * @return the default subscription mode to use for new Rosters
+ */
+ public static int getDefaultSubscriptionMode() {
+ return defaultSubscriptionMode;
+ }
+
+ /**
+ * Sets the default subscription processing mode to use when a new Roster is created. The
+ * subscription processing mode dictates what action Smack will take when subscription
+ * requests from other users are made. The default subscription mode
+ * is {@link #SUBSCRIPTION_ACCEPT_ALL}.
+ *
+ * @param subscriptionMode the default subscription mode to use for new Rosters.
+ */
+ public static void setDefaultSubscriptionMode(int subscriptionMode) {
+ defaultSubscriptionMode = subscriptionMode;
+ }
+
+ /**
+ * Creates a new roster.
+ *
+ * @param connection an XMPP connection.
+ */
+ Roster(final XMPPConnection connection) {
+ this.connection = connection;
+ groups = new Hashtable();
+ unfiledEntries = new ArrayList();
+ entries = new ArrayList();
+ rosterListeners = new ArrayList();
+ presenceMap = new HashMap();
+ // Listen for any roster packets.
+ PacketFilter rosterFilter = new PacketTypeFilter(RosterPacket.class);
+ connection.addPacketListener(new RosterPacketListener(), rosterFilter);
+ // Listen for any presence packets.
+ PacketFilter presenceFilter = new PacketTypeFilter(Presence.class);
+ connection.addPacketListener(new PresencePacketListener(), presenceFilter);
+ }
+
+ /**
+ * Returns the subscription processing mode, which dictates what action
+ * Smack will take when subscription requests from other users are made.
+ * The default subscription mode is {@link #SUBSCRIPTION_ACCEPT_ALL}.<p>
+ *
+ * If using the manual mode, a PacketListener should be registered that
+ * listens for Presence packets that have a type of
+ * {@link org.jivesoftware.smack.packet.Presence.Type#SUBSCRIBE}.
+ *
+ * @return the subscription mode.
+ */
+ public int getSubscriptionMode() {
+ return subscriptionMode;
+ }
+
+ /**
+ * Sets the subscription processing mode, which dictates what action
+ * Smack will take when subscription requests from other users are made.
+ * The default subscription mode is {@link #SUBSCRIPTION_ACCEPT_ALL}.<p>
+ *
+ * If using the manual mode, a PacketListener should be registered that
+ * listens for Presence packets that have a type of
+ * {@link org.jivesoftware.smack.packet.Presence.Type#SUBSCRIBE}.
+ *
+ * @param subscriptionMode the subscription mode.
+ */
+ public void setSubscriptionMode(int subscriptionMode) {
+ if (subscriptionMode != SUBSCRIPTION_ACCEPT_ALL &&
+ subscriptionMode != SUBSCRIPTION_REJECT_ALL &&
+ subscriptionMode != SUBSCRIPTION_MANUAL)
+ {
+ throw new IllegalArgumentException("Invalid mode.");
+ }
+ this.subscriptionMode = subscriptionMode;
+ }
+
+ /**
+ * Reloads the entire roster from the server. This is an asynchronous operation,
+ * which means the method will return immediately, and the roster will be
+ * reloaded at a later point when the server responds to the reload request.
+ */
+ public void reload() {
+ connection.sendPacket(new RosterPacket());
+ }
+
+ /**
+ * Adds a listener to this roster. The listener will be fired anytime one or more
+ * changes to the roster are pushed from the server.
+ *
+ * @param rosterListener a roster listener.
+ */
+ public void addRosterListener(RosterListener rosterListener) {
+ synchronized (rosterListeners) {
+ if (!rosterListeners.contains(rosterListener)) {
+ rosterListeners.add(rosterListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener from this roster. The listener will be fired anytime one or more
+ * changes to the roster are pushed from the server.
+ *
+ * @param rosterListener a roster listener.
+ */
+ public void removeRosterListener(RosterListener rosterListener) {
+ synchronized (rosterListeners) {
+ rosterListeners.remove(rosterListener);
+ }
+ }
+
+ /**
+ * Creates a new group.<p>
+ *
+ * Note: you must add at least one entry to the group for the group to be kept
+ * after a logout/login. This is due to the way that XMPP stores group information.
+ *
+ * @param name the name of the group.
+ * @return a new group.
+ */
+ public RosterGroup createGroup(String name) {
+ synchronized (groups) {
+ if (groups.containsKey(name)) {
+ throw new IllegalArgumentException("Group with name " + name + " alread exists.");
+ }
+ RosterGroup group = new RosterGroup(name, connection);
+ groups.put(name, group);
+ return group;
+ }
+ }
+
+ /**
+ * Creates a new roster entry and presence subscription. The server will asynchronously
+ * update the roster with the subscription status.
+ *
+ * @param user the user. (e.g. johndoe@jabber.org)
+ * @param name the nickname of the user.
+ * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the
+ * the roster entry won't belong to a group.
+ */
+ public void createEntry(String user, String name, String [] groups) throws XMPPException {
+ // Create and send roster entry creation packet.
+ RosterPacket rosterPacket = new RosterPacket();
+ rosterPacket.setType(IQ.Type.SET);
+ RosterPacket.Item item = new RosterPacket.Item(user, name);
+ if (groups != null) {
+ for (int i=0; i<groups.length; i++) {
+ if (groups[i] != null) {
+ item.addGroupName(groups[i]);
+ }
+ }
+ }
+ rosterPacket.addRosterItem(item);
+ // Wait up to a certain number of seconds for a reply from the server.
+ PacketCollector collector = connection.createPacketCollector(
+ new PacketIDFilter(rosterPacket.getPacketID()));
+ connection.sendPacket(rosterPacket);
+ IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+
+ // Create a presence subscription packet and send.
+ Presence presencePacket = new Presence(Presence.Type.SUBSCRIBE);
+ presencePacket.setTo(user);
+ connection.sendPacket(presencePacket);
+ }
+
+ /**
+ * Removes a roster entry from the roster. The roster entry will also be removed from the
+ * unfiled entries or from any roster group where it could belong and will no longer be part
+ * of the roster. Note that this is an asynchronous call -- Smack must wait for the server
+ * to send an updated subscription status.
+ *
+ * @param entry a roster entry.
+ */
+ public void removeEntry(RosterEntry entry) throws XMPPException {
+ // Only remove the entry if it's in the entry list.
+ // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet)
+ synchronized (entries) {
+ if (!entries.contains(entry)) {
+ return;
+ }
+ }
+ RosterPacket packet = new RosterPacket();
+ packet.setType(IQ.Type.SET);
+ RosterPacket.Item item = RosterEntry.toRosterItem(entry);
+ // Set the item type as REMOVE so that the server will delete the entry
+ item.setItemType(RosterPacket.ItemType.REMOVE);
+ packet.addRosterItem(item);
+ PacketCollector collector = connection.createPacketCollector(
+ new PacketIDFilter(packet.getPacketID()));
+ connection.sendPacket(packet);
+ IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ else {
+
+ }
+ }
+
+ /**
+ * Returns a count of the entries in the roster.
+ *
+ * @return the number of entries in the roster.
+ */
+ public int getEntryCount() {
+ HashMap entryMap = new HashMap();
+ // Loop through all roster groups.
+ for (Iterator groups = getGroups(); groups.hasNext(); ) {
+ RosterGroup rosterGroup = (RosterGroup) groups.next();
+ for (Iterator entries = rosterGroup.getEntries(); entries.hasNext(); ) {
+ entryMap.put(entries.next(), "");
+ }
+ }
+ synchronized (unfiledEntries) {
+ return entryMap.size() + unfiledEntries.size();
+ }
+ }
+
+ /**
+ * Returns all entries in the roster, including entries that don't belong to
+ * any groups.
+ *
+ * @return all entries in the roster.
+ */
+ public Iterator getEntries() {
+ ArrayList allEntries = new ArrayList();
+ // Loop through all roster groups and add their entries to the answer
+ for (Iterator groups = getGroups(); groups.hasNext(); ) {
+ RosterGroup rosterGroup = (RosterGroup) groups.next();
+ for (Iterator entries = rosterGroup.getEntries(); entries.hasNext(); ) {
+ RosterEntry entry = (RosterEntry)entries.next();
+ if (!allEntries.contains(entry)) {
+ allEntries.add(entry);
+ }
+ }
+ }
+ // Add the roster unfiled entries to the answer
+ synchronized (unfiledEntries) {
+ allEntries.addAll(unfiledEntries);
+ }
+ return allEntries.iterator();
+ }
+
+ /**
+ * Returns a count of the unfiled entries in the roster. An unfiled entry is
+ * an entry that doesn't belong to any groups.
+ *
+ * @return the number of unfiled entries in the roster.
+ */
+ public int getUnfiledEntryCount() {
+ synchronized (unfiledEntries) {
+ return unfiledEntries.size();
+ }
+ }
+
+ /**
+ * Returns an Iterator for the unfiled roster entries. An unfiled entry is
+ * an entry that doesn't belong to any groups.
+ *
+ * @return an iterator the unfiled roster entries.
+ */
+ public Iterator getUnfiledEntries() {
+ synchronized (unfiledEntries) {
+ return Collections.unmodifiableList(new ArrayList(unfiledEntries)).iterator();
+ }
+ }
+
+ /**
+ * Returns the roster entry associated with the given XMPP address or
+ * <tt>null</tt> if the user is not an entry in the roster.
+ *
+ * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be
+ * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource").
+ * @return the roster entry or <tt>null</tt> if it does not exist.
+ */
+ public RosterEntry getEntry(String user) {
+ if (user == null) {
+ return null;
+ }
+ String userLowerCase = user.toLowerCase();
+ synchronized (entries) {
+ for (Iterator i=entries.iterator(); i.hasNext(); ) {
+ RosterEntry entry = (RosterEntry)i.next();
+ if (entry.getUser().equals(userLowerCase)) {
+ return entry;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the specified XMPP address is an entry in the roster.
+ *
+ * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be
+ * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource").
+ * @return true if the XMPP address is an entry in the roster.
+ */
+ public boolean contains(String user) {
+ return getEntry(user) != null;
+ }
+
+ /**
+ * Returns the roster group with the specified name, or <tt>null</tt> if the
+ * group doesn't exist.
+ *
+ * @param name the name of the group.
+ * @return the roster group with the specified name.
+ */
+ public RosterGroup getGroup(String name) {
+ synchronized (groups) {
+ return (RosterGroup)groups.get(name);
+ }
+ }
+
+ /**
+ * Returns the number of the groups in the roster.
+ *
+ * @return the number of groups in the roster.
+ */
+ public int getGroupCount() {
+ synchronized (groups) {
+ return groups.size();
+ }
+ }
+
+ /**
+ * Returns an iterator the for all the roster groups.
+ *
+ * @return an iterator for all roster groups.
+ */
+ public Iterator getGroups() {
+ synchronized (groups) {
+ List groupsList = Collections.unmodifiableList(new ArrayList(groups.values()));
+ return groupsList.iterator();
+ }
+ }
+
+ /**
+ * Returns the presence info for a particular user, or <tt>null</tt> if the user
+ * is unavailable (offline) or if no presence information is available, such as
+ * when you are not subscribed to the user's presence updates.<p>
+ *
+ * If the user has several presences (one for each resource) then answer the presence
+ * with the highest priority.
+ *
+ * @param user a fully qualified xmpp ID. The address could be in any valid format (e.g.
+ * "domain/resource", "user@domain" or "user@domain/resource").
+ * @return the user's current presence, or <tt>null</tt> if the user is unavailable
+ * or if no presence information is available..
+ */
+ public Presence getPresence(String user) {
+ String key = getPresenceMapKey(user);
+ Map userPresences = (Map) presenceMap.get(key);
+ if (userPresences == null) {
+ return null;
+ }
+ else {
+ // Find the resource with the highest priority
+ // Might be changed to use the resource with the highest availability instead.
+ Iterator it = userPresences.keySet().iterator();
+ Presence p;
+ Presence presence = null;
+
+ while (it.hasNext()) {
+ p = (Presence) userPresences.get(it.next());
+ if (presence == null) {
+ presence = p;
+ }
+ else {
+ if (p.getPriority() > presence.getPriority()) {
+ presence = p;
+ }
+ }
+ }
+ return presence;
+ }
+ }
+
+ /**
+ * Returns the presence info for a particular user's resource, or <tt>null</tt> if the user
+ * is unavailable (offline) or if no presence information is available, such as
+ * when you are not subscribed to the user's presence updates.
+ *
+ * @param userResource a fully qualified xmpp ID including a resource.
+ * @return the user's current presence, or <tt>null</tt> if the user is unavailable
+ * or if no presence information is available.
+ */
+ public Presence getPresenceResource(String userResource) {
+ String key = getPresenceMapKey(userResource);
+ String resource = StringUtils.parseResource(userResource);
+ Map userPresences = (Map)presenceMap.get(key);
+ if (userPresences == null) {
+ return null;
+ }
+ else {
+ return (Presence) userPresences.get(resource);
+ }
+ }
+
+ /**
+ * Returns an iterator (of Presence objects) for all the user's current presences
+ * or <tt>null</tt> if the user is unavailable (offline) or if no presence information
+ * is available, such as when you are not subscribed to the user's presence updates.
+ *
+ * @param user a fully qualified xmpp ID, e.g. jdoe@example.com
+ * @return an iterator (of Presence objects) for all the user's current presences,
+ * or <tt>null</tt> if the user is unavailable or if no presence information
+ * is available.
+ */
+ public Iterator getPresences(String user) {
+ String key = getPresenceMapKey(user);
+ Map userPresences = (Map)presenceMap.get(key);
+ if (userPresences == null) {
+ return null;
+ }
+ else {
+ synchronized (userPresences) {
+ return new HashMap(userPresences).values().iterator();
+ }
+ }
+ }
+
+ /**
+ * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster
+ * can contain any valid address format such us "domain/resource", "user@domain" or
+ * "user@domain/resource". If the roster contains an entry associated with the fully qualified
+ * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the
+ * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the
+ * userPresences is useless since it will always contain one entry for the user.
+ *
+ * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work.
+ * @return the key to use in the presenceMap for the fully qualified xmpp ID.
+ */
+ private String getPresenceMapKey(String user) {
+ if (user == null) {
+ return null;
+ }
+ String key = user;
+ if (!contains(user)) {
+ key = StringUtils.parseBareAddress(user);
+ }
+ return key.toLowerCase();
+ }
+
+ /**
+ * Fires roster changed event to roster listeners indicating that the
+ * specified collections of contacts have been added, updated or deleted
+ * from the roster.
+ *
+ * @param addedEntries the collection of address of the added contacts.
+ * @param updatedEntries the collection of address of the updated contacts.
+ * @param deletedEntries the collection of address of the deleted contacts.
+ */
+ private void fireRosterChangedEvent(Collection addedEntries, Collection updatedEntries,
+ Collection deletedEntries) {
+ RosterListener [] listeners = null;
+ synchronized (rosterListeners) {
+ listeners = new RosterListener[rosterListeners.size()];
+ rosterListeners.toArray(listeners);
+ }
+ for (int i=0; i<listeners.length; i++) {
+ if (!addedEntries.isEmpty()) {
+ listeners[i].entriesAdded(addedEntries);
+ }
+ if (!updatedEntries.isEmpty()) {
+ listeners[i].entriesUpdated(updatedEntries);
+ }
+ if (!deletedEntries.isEmpty()) {
+ listeners[i].entriesDeleted(deletedEntries);
+ }
+ }
+ }
+
+ /**
+ * Fires roster presence changed event to roster listeners.
+ */
+ private void fireRosterPresenceEvent(String user) {
+ RosterListener [] listeners = null;
+ synchronized (rosterListeners) {
+ listeners = new RosterListener[rosterListeners.size()];
+ rosterListeners.toArray(listeners);
+ }
+ for (int i=0; i<listeners.length; i++) {
+ listeners[i].presenceChanged(user);
+ }
+ }
+
+ /**
+ * Listens for all presence packets and processes them.
+ */
+ private class PresencePacketListener implements PacketListener {
+ public void processPacket(Packet packet) {
+ Presence presence = (Presence)packet;
+ String from = presence.getFrom();
+ String key = getPresenceMapKey(from);
+
+ // If an "available" packet, add it to the presence map. Each presence map will hold
+ // for a particular user a map with the presence packets saved for each resource.
+ if (presence.getType() == Presence.Type.AVAILABLE) {
+ Map userPresences;
+ // Get the user presence map
+ if (presenceMap.get(key) == null) {
+ userPresences = new HashMap();
+ presenceMap.put(key, userPresences);
+ }
+ else {
+ userPresences = (Map)presenceMap.get(key);
+ }
+ // Add the new presence, using the resources as a key.
+ synchronized (userPresences) {
+ userPresences.put(StringUtils.parseResource(from), presence);
+ }
+ // If the user is in the roster, fire an event.
+ synchronized (entries) {
+ for (Iterator i = entries.iterator(); i.hasNext();) {
+ RosterEntry entry = (RosterEntry) i.next();
+ if (entry.getUser().equals(key)) {
+ fireRosterPresenceEvent(from);
+ }
+ }
+ }
+ }
+ // If an "unavailable" packet, remove any entries in the presence map.
+ else if (presence.getType() == Presence.Type.UNAVAILABLE) {
+ if (presenceMap.get(key) != null) {
+ Map userPresences = (Map) presenceMap.get(key);
+ synchronized (userPresences) {
+ userPresences.remove(StringUtils.parseResource(from));
+ }
+ if (userPresences.isEmpty()) {
+ presenceMap.remove(key);
+ }
+ }
+ // If the user is in the roster, fire an event.
+ synchronized (entries) {
+ for (Iterator i=entries.iterator(); i.hasNext(); ) {
+ RosterEntry entry = (RosterEntry)i.next();
+ if (entry.getUser().equals(key)) {
+ fireRosterPresenceEvent(from);
+ }
+ }
+ }
+ }
+ else if (presence.getType() == Presence.Type.SUBSCRIBE) {
+ if (subscriptionMode == SUBSCRIPTION_ACCEPT_ALL) {
+ // Accept all subscription requests.
+ Presence response = new Presence(Presence.Type.SUBSCRIBED);
+ response.setTo(presence.getFrom());
+ connection.sendPacket(response);
+ }
+ else if (subscriptionMode == SUBSCRIPTION_REJECT_ALL) {
+ // Reject all subscription requests.
+ Presence response = new Presence(Presence.Type.UNSUBSCRIBED);
+ response.setTo(presence.getFrom());
+ connection.sendPacket(response);
+ }
+ // Otherwise, in manual mode so ignore.
+ }
+ else if (presence.getType() == Presence.Type.UNSUBSCRIBE) {
+ if (subscriptionMode != SUBSCRIPTION_MANUAL) {
+ // Acknowledge and accept unsubscription notification so that the
+ // server will stop sending notifications saying that the contact
+ // has unsubscribed to our presence.
+ Presence response = new Presence(Presence.Type.UNSUBSCRIBED);
+ response.setTo(presence.getFrom());
+ connection.sendPacket(response);
+ }
+ // Otherwise, in manual mode so ignore.
+ }
+ }
+ }
+
+ /**
+ * Listens for all roster packets and processes them.
+ */
+ private class RosterPacketListener implements PacketListener {
+
+ public void processPacket(Packet packet) {
+ // Keep a registry of the entries that were added, deleted or updated. An event
+ // will be fired for each affected entry
+ Collection addedEntries = new ArrayList();
+ Collection updatedEntries = new ArrayList();
+ Collection deletedEntries = new ArrayList();
+
+ RosterPacket rosterPacket = (RosterPacket)packet;
+ for (Iterator i=rosterPacket.getRosterItems(); i.hasNext(); ) {
+ RosterPacket.Item item = (RosterPacket.Item)i.next();
+ RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
+ item.getItemType(), item.getItemStatus(), connection);
+
+ // If the packet is of the type REMOVE then remove the entry
+ if (RosterPacket.ItemType.REMOVE.equals(item.getItemType())) {
+ // Remove the entry from the entry list.
+ if (entries.contains(entry)) {
+ entries.remove(entry);
+ }
+ // Remove the entry from the unfiled entry list.
+ synchronized (unfiledEntries) {
+ if (unfiledEntries.contains(entry)) {
+ unfiledEntries.remove(entry);
+ }
+ }
+ // Removing the user from the roster, so remove any presence information
+ // about them.
+ String key = StringUtils.parseName(item.getUser()) + "@" +
+ StringUtils.parseServer(item.getUser());
+ presenceMap.remove(key);
+ // Keep note that an entry has been removed
+ deletedEntries.add(item.getUser());
+ }
+ else {
+ // Make sure the entry is in the entry list.
+ if (!entries.contains(entry)) {
+ entries.add(entry);
+ // Keep note that an entry has been added
+ addedEntries.add(item.getUser());
+ }
+ else {
+ // If the entry was in then list then update its state with the new values
+ RosterEntry existingEntry =
+ (RosterEntry) entries.get(entries.indexOf(entry));
+ existingEntry
+ .updateState(entry.getName(), entry.getType(), entry.getStatus());
+ // Keep note that an entry has been updated
+ updatedEntries.add(item.getUser());
+ }
+ // If the roster entry belongs to any groups, remove it from the
+ // list of unfiled entries.
+ if (item.getGroupNames().hasNext()) {
+ synchronized (unfiledEntries) {
+ unfiledEntries.remove(entry);
+ }
+ }
+ // Otherwise add it to the list of unfiled entries.
+ else {
+ synchronized (unfiledEntries) {
+ if (!unfiledEntries.contains(entry)) {
+ unfiledEntries.add(entry);
+ }
+ }
+ }
+ }
+
+ // Find the list of groups that the user currently belongs to.
+ List currentGroupNames = new ArrayList();
+ for (Iterator j = entry.getGroups(); j.hasNext(); ) {
+ RosterGroup group = (RosterGroup)j.next();
+ currentGroupNames.add(group.getName());
+ }
+
+ // If the packet is not of the type REMOVE then add the entry to the groups
+ if (!RosterPacket.ItemType.REMOVE.equals(item.getItemType())) {
+ // Create the new list of groups the user belongs to.
+ List newGroupNames = new ArrayList();
+ for (Iterator k = item.getGroupNames(); k.hasNext(); ) {
+ String groupName = (String)k.next();
+ // Add the group name to the list.
+ newGroupNames.add(groupName);
+
+ // Add the entry to the group.
+ RosterGroup group = getGroup(groupName);
+ if (group == null) {
+ group = createGroup(groupName);
+ groups.put(groupName, group);
+ }
+ // Add the entry.
+ group.addEntryLocal(entry);
+ }
+
+ // We have the list of old and new group names. We now need to
+ // remove the entry from the all the groups it may no longer belong
+ // to. We do this by subracting the new group set from the old.
+ for (int m=0; m<newGroupNames.size(); m++) {
+ currentGroupNames.remove(newGroupNames.get(m));
+ }
+ }
+
+ // Loop through any groups that remain and remove the entries.
+ // This is neccessary for the case of remote entry removals.
+ for (int n=0; n<currentGroupNames.size(); n++) {
+ String groupName = (String)currentGroupNames.get(n);
+ RosterGroup group = getGroup(groupName);
+ group.removeEntryLocal(entry);
+ if (group.getEntryCount() == 0) {
+ synchronized (groups) {
+ groups.remove(groupName);
+ }
+ }
+ }
+ // Remove all the groups with no entries. We have to do this because
+ // RosterGroup.removeEntry removes the entry immediately (locally) and the
+ // group could remain empty.
+ // TODO Check the performance/logic for rosters with large number of groups
+ for (Iterator it = getGroups(); it.hasNext();) {
+ RosterGroup group = (RosterGroup)it.next();
+ if (group.getEntryCount() == 0) {
+ synchronized (groups) {
+ groups.remove(group.getName());
+ }
+ }
+ }
+ }
+
+ // Mark the roster as initialized.
+ synchronized (Roster.this) {
+ rosterInitialized = true;
+ Roster.this.notifyAll();
+ }
+
+ // Fire event for roster listeners.
+ fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterEntry.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterEntry.java
new file mode 100644
index 000000000..d972c2bf6
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterEntry.java
@@ -0,0 +1,193 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.packet.IQ;
+
+import java.util.*;
+
+/**
+ * Each user in your roster is represented by a roster entry, which contains the user's
+ * JID and a name or nickname you assign.
+ *
+ * @author Matt Tucker
+ */
+public class RosterEntry {
+
+ private String user;
+ private String name;
+ private RosterPacket.ItemType type;
+ private RosterPacket.ItemStatus status;
+ private XMPPConnection connection;
+
+ /**
+ * Creates a new roster entry.
+ *
+ * @param user the user.
+ * @param name the nickname for the entry.
+ * @param type the subscription type.
+ * @param status the subscription status (related to subscriptions pending to be approbed).
+ * @param connection a connection to the XMPP server.
+ */
+ RosterEntry(String user, String name, RosterPacket.ItemType type,
+ RosterPacket.ItemStatus status, XMPPConnection connection) {
+ this.user = user;
+ this.name = name;
+ this.type = type;
+ this.status = status;
+ this.connection = connection;
+ }
+
+ /**
+ * Returns the JID of the user associated with this entry.
+ *
+ * @return the user associated with this entry.
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * Returns the name associated with this entry.
+ *
+ * @return the name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name associated with this entry.
+ *
+ * @param name the name.
+ */
+ public void setName(String name) {
+ // Do nothing if the name hasn't changed.
+ if (name != null && name.equals(this.name)) {
+ return;
+ }
+ this.name = name;
+ RosterPacket packet = new RosterPacket();
+ packet.setType(IQ.Type.SET);
+ packet.addRosterItem(toRosterItem(this));
+ connection.sendPacket(packet);
+ }
+
+ /**
+ * Updates the state of the entry with the new values.
+ *
+ * @param name the nickname for the entry.
+ * @param type the subscription type.
+ * @param status the subscription status (related to subscriptions pending to be approbed).
+ */
+ void updateState(String name, RosterPacket.ItemType type, RosterPacket.ItemStatus status) {
+ this.name = name;
+ this.type = type;
+ this.status = status;
+ }
+
+ /**
+ * Returns an iterator for all the roster groups that this entry belongs to.
+ *
+ * @return an iterator for the groups this entry belongs to.
+ */
+ public Iterator getGroups() {
+ List results = new ArrayList();
+ // Loop through all roster groups and find the ones that contain this
+ // entry. This algorithm should be fine
+ for (Iterator i=connection.roster.getGroups(); i.hasNext(); ) {
+ RosterGroup group = (RosterGroup)i.next();
+ if (group.contains(this)) {
+ results.add(group);
+ }
+ }
+ return results.iterator();
+ }
+
+ /**
+ * Returns the roster subscription type of the entry. When the type is
+ * {@link RosterPacket.ItemType#NONE} or {@link RosterPacket.ItemType#FROM},
+ * refer to {@link RosterEntry getStatus()} to see if a subscription request
+ * is pending.
+ *
+ * @return the type.
+ */
+ public RosterPacket.ItemType getType() {
+ return type;
+ }
+
+ /**
+ * Returns the roster subscription status of the entry. When the status is
+ * RosterPacket.ItemStatus.SUBSCRIPTION_PENDING, the contact has to answer the
+ * subscription request.
+ *
+ * @return the status.
+ */
+ public RosterPacket.ItemStatus getStatus() {
+ return status;
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ if (name != null) {
+ buf.append(name).append(": ");
+ }
+ buf.append(user);
+ Iterator groups = getGroups();
+ if (groups.hasNext()) {
+ buf.append(" [");
+ RosterGroup group = (RosterGroup)groups.next();
+ buf.append(group.getName());
+ while (groups.hasNext()) {
+ buf.append(", ");
+ group = (RosterGroup)groups.next();
+ buf.append(group.getName());
+ }
+ buf.append("]");
+ }
+ return buf.toString();
+ }
+
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object != null && object instanceof RosterEntry) {
+ return user.equals(((RosterEntry)object).getUser());
+ }
+ else {
+ return false;
+ }
+ }
+
+ static RosterPacket.Item toRosterItem(RosterEntry entry) {
+ RosterPacket.Item item = new RosterPacket.Item(entry.getUser(), entry.getName());
+ item.setItemType(entry.getType());
+ item.setItemStatus(entry.getStatus());
+ // Set the correct group names for the item.
+ for (Iterator j=entry.getGroups(); j.hasNext(); ) {
+ RosterGroup group = (RosterGroup)j.next();
+ item.addGroupName(group.getName());
+ }
+ return item;
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterGroup.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterGroup.java
new file mode 100644
index 000000000..c84dd15c9
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterGroup.java
@@ -0,0 +1,252 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+
+import java.util.*;
+
+/**
+ * A group of roster entries.
+ *
+ * @see Roster#getGroup(String)
+ * @author Matt Tucker
+ */
+public class RosterGroup {
+
+ private String name;
+ private XMPPConnection connection;
+ private List entries;
+
+ /**
+ * Creates a new roster group instance.
+ *
+ * @param name the name of the group.
+ * @param connection the connection the group belongs to.
+ */
+ RosterGroup(String name, XMPPConnection connection) {
+ this.name = name;
+ this.connection = connection;
+ entries = new ArrayList();
+ }
+
+ /**
+ * Returns the name of the group.
+ *
+ * @return the name of the group.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name of the group. Changing the group's name is like moving all the group entries
+ * of the group to a new group specified by the new name. Since this group won't have entries
+ * it will be removed from the roster. This means that all the references to this object will
+ * be invalid and will need to be updated to the new group specified by the new name.
+ *
+ * @param name the name of the group.
+ */
+ public void setName(String name) {
+ synchronized (entries) {
+ for (int i=0; i<entries.size(); i++) {
+ RosterPacket packet = new RosterPacket();
+ packet.setType(IQ.Type.SET);
+ RosterEntry entry = (RosterEntry)entries.get(i);
+ RosterPacket.Item item = RosterEntry.toRosterItem(entry);
+ item.removeGroupName(this.name);
+ item.addGroupName(name);
+ packet.addRosterItem(item);
+ connection.sendPacket(packet);
+ }
+ }
+ }
+
+ /**
+ * Returns the number of entries in the group.
+ *
+ * @return the number of entries in the group.
+ */
+ public int getEntryCount() {
+ synchronized (entries) {
+ return entries.size();
+ }
+ }
+
+ /**
+ * Returns an iterator for the entries in the group.
+ *
+ * @return an iterator for the entries in the group.
+ */
+ public Iterator getEntries() {
+ synchronized (entries) {
+ return Collections.unmodifiableList(new ArrayList(entries)).iterator();
+ }
+ }
+
+ /**
+ * Returns the roster entry associated with the given XMPP address or
+ * <tt>null</tt> if the user is not an entry in the group.
+ *
+ * @param user the XMPP address of the user (eg "jsmith@example.com").
+ * @return the roster entry or <tt>null</tt> if it does not exist in the group.
+ */
+ public RosterEntry getEntry(String user) {
+ if (user == null) {
+ return null;
+ }
+ // Roster entries never include a resource so remove the resource
+ // if it's a part of the XMPP address.
+ user = StringUtils.parseBareAddress(user);
+ String userLowerCase = user.toLowerCase();
+ synchronized (entries) {
+ for (Iterator i=entries.iterator(); i.hasNext(); ) {
+ RosterEntry entry = (RosterEntry)i.next();
+ if (entry.getUser().equals(userLowerCase)) {
+ return entry;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the specified entry is part of this group.
+ *
+ * @param entry a roster entry.
+ * @return true if the entry is part of this group.
+ */
+ public boolean contains(RosterEntry entry) {
+ synchronized (entries) {
+ return entries.contains(entry);
+ }
+ }
+
+ /**
+ * Returns true if the specified XMPP address is an entry in this group.
+ *
+ * @param user the XMPP address of the user.
+ * @return true if the XMPP address is an entry in this group.
+ */
+ public boolean contains(String user) {
+ return getEntry(user) != null;
+ }
+
+ /**
+ * Adds a roster entry to this group. If the entry was unfiled then it will be removed from
+ * the unfiled list and will be added to this group.
+ *
+ * @param entry a roster entry.
+ * @throws XMPPException if an error occured while trying to add the entry to the group.
+ */
+ public void addEntry(RosterEntry entry) throws XMPPException {
+ PacketCollector collector = null;
+ // Only add the entry if it isn't already in the list.
+ synchronized (entries) {
+ if (!entries.contains(entry)) {
+ RosterPacket packet = new RosterPacket();
+ packet.setType(IQ.Type.SET);
+ RosterPacket.Item item = RosterEntry.toRosterItem(entry);
+ item.addGroupName(getName());
+ packet.addRosterItem(item);
+ // Wait up to a certain number of seconds for a reply from the server.
+ collector = connection
+ .createPacketCollector(new PacketIDFilter(packet.getPacketID()));
+ connection.sendPacket(packet);
+ }
+ }
+ if (collector != null) {
+ IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ // Add the new entry to the group since the server processed the request successfully
+ addEntryLocal(entry);
+ }
+ }
+
+ /**
+ * Removes a roster entry from this group. If the entry does not belong to any other group
+ * then it will be considered as unfiled, therefore it will be added to the list of unfiled
+ * entries.
+ *
+ * @param entry a roster entry.
+ * @throws XMPPException if an error occured while trying to remove the entry from the group.
+ */
+ public void removeEntry(RosterEntry entry) throws XMPPException {
+ PacketCollector collector = null;
+ // Only remove the entry if it's in the entry list.
+ // Remove the entry locally, if we wait for RosterPacketListenerprocess>>Packet(Packet)
+ // to take place the entry will exist in the group until a packet is received from the
+ // server.
+ synchronized (entries) {
+ if (entries.contains(entry)) {
+ RosterPacket packet = new RosterPacket();
+ packet.setType(IQ.Type.SET);
+ RosterPacket.Item item = RosterEntry.toRosterItem(entry);
+ item.removeGroupName(this.getName());
+ packet.addRosterItem(item);
+ // Wait up to a certain number of seconds for a reply from the server.
+ collector = connection
+ .createPacketCollector(new PacketIDFilter(packet.getPacketID()));
+ connection.sendPacket(packet);
+ }
+ }
+ if (collector != null) {
+ IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ // Remove the entry locally since the server processed the request successfully
+ removeEntryLocal(entry);
+ }
+ }
+
+ void addEntryLocal(RosterEntry entry) {
+ // Only add the entry if it isn't already in the list.
+ synchronized (entries) {
+ entries.remove(entry);
+ entries.add(entry);
+ }
+ }
+
+ void removeEntryLocal(RosterEntry entry) {
+ // Only remove the entry if it's in the entry list.
+ synchronized (entries) {
+ if (entries.contains(entry)) {
+ entries.remove(entry);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterListener.java
new file mode 100644
index 000000000..fbc6ce8e7
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterListener.java
@@ -0,0 +1,62 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 java.util.Collection;
+
+/**
+ * A listener that is fired any time a roster is changed or the presence of
+ * a user in the roster is changed.
+ *
+ * @author Matt Tucker
+ */
+public interface RosterListener {
+
+ /**
+ * Called when roster entries are added.
+ *
+ * @param addresses the XMPP addresses of the contacts that have been added to the roster.
+ */
+ public void entriesAdded(Collection addresses);
+
+ /**
+ * Called when a roster entries are updated.
+ *
+ * @param addresses the XMPP addresses of the contacts whose entries have been updated.
+ */
+ public void entriesUpdated(Collection addresses);
+
+ /**
+ * Called when a roster entries are removed.
+ *
+ * @param addresses the XMPP addresses of the contacts that have been removed from the roster.
+ */
+ public void entriesDeleted(Collection addresses);
+
+ /**
+ * Called when the presence of a roster entry is changed.
+ *
+ * @param XMPPAddress the XMPP address of the user who's presence has changed,
+ * including the resource.
+ */
+ public void presenceChanged(String XMPPAddress);
+}
+
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SASLAuthentication.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SASLAuthentication.java
new file mode 100644
index 000000000..03d8c0928
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SASLAuthentication.java
@@ -0,0 +1,417 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.Bind;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Session;
+import org.jivesoftware.smack.sasl.SASLAnonymous;
+import org.jivesoftware.smack.sasl.SASLMechanism;
+import org.jivesoftware.smack.sasl.SASLPlainMechanism;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.*;
+
+/**
+ * This class is responsible authenticating the user using SASL, binding the resource
+ * to the connection and establishing a session with the server.<p>
+ *
+ * Once TLS has been negotiated (i.e. the connection has been secured) it is possible to
+ * register with the server, authenticate using Non-SASL or authenticate using SASL. If the
+ * server supports SASL then Smack will first try to authenticate using SASL. But if that
+ * fails then Non-SASL will be tried.<p>
+ *
+ * The server may support many SASL mechanisms to use for authenticating. Out of the box
+ * Smack provides SASL PLAIN but it is possible to register new SASL Mechanisms. Use
+ * {@link #registerSASLMechanism(int, String, Class)} to add new mechanisms. See
+ * {@link SASLMechanism}.<p>
+ *
+ * Once the user has been authenticated with SASL, it is necessary to bind a resource for
+ * the connection. If no resource is passed in {@link #authenticate(String, String, String)}
+ * then the server will assign a resource for the connection. In case a resource is passed
+ * then the server will receive the desired resource but may assign a modified resource for
+ * the connection.<p>
+ *
+ * Once a resource has been binded and if the server supports sessions then Smack will establish
+ * a session so that instant messaging and presence functionalities may be used.
+ *
+ * @author Gaston Dombiak
+ */
+public class SASLAuthentication implements UserAuthentication {
+
+ private static Map implementedMechanisms = new HashMap();
+ private static List mechanismsPreferences = new ArrayList();
+
+ private XMPPConnection connection;
+ private Collection serverMechanisms = new ArrayList();
+ private SASLMechanism currentMechanism = null;
+ /**
+ * Boolean indicating if SASL negotiation has finished and was successful.
+ */
+ private boolean saslNegotiated = false;
+ /**
+ * Boolean indication if SASL authentication has failed. When failed the server may end
+ * the connection.
+ */
+ private boolean saslFailed = false;
+ private boolean resourceBinded = false;
+ private boolean sessionSupported = false;
+
+ static {
+ // Register SASL mechanisms supported by Smack
+ registerSASLMechanism(0, "PLAIN", SASLPlainMechanism.class);
+ }
+
+ /**
+ * Registers a new SASL mechanism in the specified preference position. The client will try
+ * to authenticate using the most prefered SASL mechanism that is also supported by the server.
+ * <p/>
+ * <p/>
+ * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism.
+ * A value of 0 means that the mechanism is the most prefered one.
+ *
+ * @param index preference position amongst all the implemented SASL mechanism. Starts with 0.
+ * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ * @param mClass a SASLMechanism subclass.
+ */
+ public static void registerSASLMechanism(int index, String name, Class mClass) {
+ implementedMechanisms.put(name, mClass);
+ mechanismsPreferences.add(index, name);
+ }
+
+ /**
+ * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't
+ * be possible to authenticate users using the removed SASL mechanism.
+ *
+ * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ */
+ public static void unregisterSASLMechanism(String name) {
+ implementedMechanisms.remove(name);
+ mechanismsPreferences.remove(name);
+ }
+
+ /**
+ * Returns the registerd SASLMechanism classes sorted by the level of preference.
+ *
+ * @return the registerd SASLMechanism classes sorted by the level of preference.
+ */
+ public static List getRegisterSASLMechanisms() {
+ List answer = new ArrayList();
+ for (Iterator it = mechanismsPreferences.iterator(); it.hasNext();) {
+ answer.add(implementedMechanisms.get(it.next()));
+ }
+ return answer;
+ }
+
+ SASLAuthentication(XMPPConnection connection) {
+ super();
+ this.connection = connection;
+ }
+
+ /**
+ * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.
+ *
+ * @return true if the server offered ANONYMOUS SASL as a way to authenticate users.
+ */
+ public boolean hasAnonymousAuthentication() {
+ return serverMechanisms.contains("ANONYMOUS");
+ }
+
+ /**
+ * Returns true if the server offered SASL authentication besides ANONYMOUS SASL.
+ *
+ * @return true if the server offered SASL authentication besides ANONYMOUS SASL.
+ */
+ public boolean hasNonAnonymousAuthentication() {
+ if (!serverMechanisms.isEmpty()) {
+ // Check that anonymous sasl is not the only supported mechanism
+ if (serverMechanisms.size() == 1) {
+ return !hasAnonymousAuthentication();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs SASL authentication of the specified user. If SASL authentication was successful
+ * then resource binding and session establishment will be performed. This method will return
+ * the full JID provided by the server while binding a resource to the connection.<p>
+ *
+ * The server may assign a full JID with a username or resource different than the requested
+ * by this method.
+ *
+ * @param username the username that is authenticating with the server.
+ * @param password the password to send to the server.
+ * @param resource the desired resource.
+ * @return the full JID provided by the server while binding a resource to the connection.
+ * @throws XMPPException if an error occures while authenticating.
+ */
+ public String authenticate(String username, String password, String resource)
+ throws XMPPException {
+ // Locate the SASLMechanism to use
+ Class selected = null;
+ for (Iterator it = mechanismsPreferences.iterator(); it.hasNext();) {
+ String mechanism = (String) it.next();
+ if (implementedMechanisms.containsKey(mechanism) &&
+ serverMechanisms.contains(mechanism)) {
+ selected = (Class) implementedMechanisms.get(mechanism);
+ break;
+ }
+ }
+ if (selected != null) {
+ // A SASL mechanism was found. Authenticate using the selected mechanism and then
+ // proceed to bind a resource
+ try {
+ Constructor constructor = selected
+ .getConstructor(new Class[]{SASLAuthentication.class});
+ currentMechanism = (SASLMechanism) constructor.newInstance(new Object[]{this});
+ // Trigger SASL authentication with the selected mechanism
+ currentMechanism.authenticate(username, connection.getServiceName(), password);
+
+ // Wait until SASL negotiation finishes
+ synchronized (this) {
+ if (!saslNegotiated && !saslFailed) {
+ try {
+ wait(30000);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ if (saslFailed) {
+ // SASL authentication failed and the server may have closed the connection
+ // so throw an exception
+ throw new XMPPException("SASL authentication failed");
+ }
+
+ if (saslNegotiated) {
+ // Bind a resource for this connection and
+ return bindResourceAndEstablishSession(resource);
+ } else {
+ // SASL authentication failed so try a Non-SASL authentication
+ return new NonSASLAuthentication(connection)
+ .authenticate(username, password, resource);
+ }
+ }
+ catch (XMPPException e) {
+ throw e;
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ // SASL authentication failed so try a Non-SASL authentication
+ return new NonSASLAuthentication(connection)
+ .authenticate(username, password, resource);
+ }
+ } else {
+ // No SASL method was found so try a Non-SASL authentication
+ return new NonSASLAuthentication(connection).authenticate(username, password, resource);
+ }
+ }
+
+ /**
+ * Performs ANONYMOUS SASL authentication. If SASL authentication was successful
+ * then resource binding and session establishment will be performed. This method will return
+ * the full JID provided by the server while binding a resource to the connection.<p>
+ *
+ * The server will assign a full JID with a randomly generated resource and possibly with
+ * no username.
+ *
+ * @return the full JID provided by the server while binding a resource to the connection.
+ * @throws XMPPException if an error occures while authenticating.
+ */
+ public String authenticateAnonymously() throws XMPPException {
+ try {
+ currentMechanism = new SASLAnonymous(this);
+ currentMechanism.authenticate(null, null, null);
+
+ // Wait until SASL negotiation finishes
+ synchronized (this) {
+ if (!saslNegotiated && !saslFailed) {
+ try {
+ wait(5000);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ if (saslFailed) {
+ // SASL authentication failed and the server may have closed the connection
+ // so throw an exception
+ throw new XMPPException("SASL authentication failed");
+ }
+
+ if (saslNegotiated) {
+ // Bind a resource for this connection and
+ return bindResourceAndEstablishSession(null);
+ }
+ else {
+ return new NonSASLAuthentication(connection).authenticateAnonymously();
+ }
+ } catch (IOException e) {
+ return new NonSASLAuthentication(connection).authenticateAnonymously();
+ }
+ }
+
+ private String bindResourceAndEstablishSession(String resource) throws XMPPException {
+ // Wait until server sends response containing the <bind> element
+ synchronized (this) {
+ if (!resourceBinded) {
+ try {
+ wait(30000);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ if (!resourceBinded) {
+ // Server never offered resource binding
+ throw new XMPPException("Resource binding not offered by server");
+ }
+
+ Bind bindResource = new Bind();
+ bindResource.setResource(resource);
+
+ PacketCollector collector = connection
+ .createPacketCollector(new PacketIDFilter(bindResource.getPacketID()));
+ // Send the packet
+ connection.sendPacket(bindResource);
+ // Wait up to a certain number of seconds for a response from the server.
+ Bind response = (Bind) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ String userJID = response.getJid();
+
+ if (sessionSupported) {
+ Session session = new Session();
+ collector = connection.createPacketCollector(new PacketIDFilter(session.getPacketID()));
+ // Send the packet
+ connection.sendPacket(session);
+ // Wait up to a certain number of seconds for a response from the server.
+ IQ ack = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (ack == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (ack.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(ack.getError());
+ }
+ }
+ else {
+ // Server never offered session establishment
+ throw new XMPPException("Session establishment not offered by server");
+ }
+ return userJID;
+ }
+
+ /**
+ * Sets the available SASL mechanism reported by the server. The server will report the
+ * available SASL mechanism once the TLS negotiation was successful. This information is
+ * stored and will be used when doing the authentication for logging in the user.
+ *
+ * @param mechanisms collection of strings with the available SASL mechanism reported
+ * by the server.
+ */
+ void setAvailableSASLMethods(Collection mechanisms) {
+ this.serverMechanisms = mechanisms;
+ }
+
+ /**
+ * Returns true if the user was able to authenticate with the server usins SASL.
+ *
+ * @return true if the user was able to authenticate with the server usins SASL.
+ */
+ public boolean isAuthenticated() {
+ return saslNegotiated;
+ }
+
+ /**
+ * The server is challenging the SASL authentication we just sent. Forward the challenge
+ * to the current SASLMechanism we are using. The SASLMechanism will send a response to
+ * the server. The length of the challenge-response sequence varies according to the
+ * SASLMechanism in use.
+ *
+ * @param challenge a base64 encoded string representing the challenge.
+ * @throws IOException If a network error occures while authenticating.
+ */
+ void challengeReceived(String challenge) throws IOException {
+ currentMechanism.challengeReceived(challenge);
+ }
+
+ /**
+ * Notification message saying that SASL authentication was successful. The next step
+ * would be to bind the resource.
+ */
+ void authenticated() {
+ synchronized (this) {
+ saslNegotiated = true;
+ // Wake up the thread that is waiting in the #authenticate method
+ notify();
+ }
+ }
+
+ /**
+ * Notification message saying that SASL authentication has failed. The server may have
+ * closed the connection depending on the number of possible retries.
+ */
+ void authenticationFailed() {
+ synchronized (this) {
+ saslFailed = true;
+ // Wake up the thread that is waiting in the #authenticate method
+ notify();
+ }
+ }
+
+ /**
+ * Notification message saying that the server requires the client to bind a
+ * resource to the stream.
+ */
+ void bindingRequired() {
+ synchronized (this) {
+ resourceBinded = true;
+ // Wake up the thread that is waiting in the #authenticate method
+ notify();
+ }
+ }
+
+ public void send(String stanza) throws IOException {
+ connection.writer.write(stanza);
+ connection.writer.flush();
+ }
+
+ /**
+ * Notification message saying that the server supports sessions. When a server supports
+ * sessions the client needs to send a Session packet after successfully binding a resource
+ * for the session.
+ */
+ void sessionsSupported() {
+ sessionSupported = true;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SSLXMPPConnection.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SSLXMPPConnection.java
new file mode 100644
index 000000000..bb3a72f98
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SSLXMPPConnection.java
@@ -0,0 +1,160 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Creates an SSL connection to a XMPP server using the legacy dedicated SSL port
+ * mechanism. Fully compliant XMPP 1.0 servers (e.g. Wildfire 2.4.0) do not
+ * require using a dedicated SSL port. Instead, TLS (a standardized version of SSL 3.0)
+ * is dynamically negotiated over the standard XMPP port. Therefore, only use this
+ * class to connect to an XMPP server if you know that the server does not support
+ * XMPP 1.0 TLS connections.
+ *
+ * @author Matt Tucker
+ */
+public class SSLXMPPConnection extends XMPPConnection {
+
+ private static SocketFactory socketFactory = new DummySSLSocketFactory();
+
+ /**
+ * Creates a new SSL connection to the specified host on the default
+ * SSL port (5223). The IP address of the server is assumed to match the
+ * service name.
+ *
+ * @param host the XMPP host.
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropiate error messages to end-users.
+ */
+ public SSLXMPPConnection(String host) throws XMPPException {
+ this(host, 5223);
+ }
+
+ /**
+ * Creates a new SSL connection to the specified host on the specified port. The IP address
+ * of the server is assumed to match the service name.
+ *
+ * @param host the XMPP host.
+ * @param port the port to use for the connection (default XMPP SSL port is 5223).
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropiate error messages to end-users.
+ */
+ public SSLXMPPConnection(String host, int port) throws XMPPException {
+ this(host, port, host);
+ }
+
+ /**
+ * Creates a new SSL connection to the specified XMPP server on the given host and port.
+ *
+ * @param host the host name, or null for the loopback address.
+ * @param port the port on the server that should be used (default XMPP SSL port is 5223).
+ * @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>.
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropiate error messages to end-users.
+ */
+ public SSLXMPPConnection(String host, int port, String serviceName) throws XMPPException {
+ super(host, port, serviceName, socketFactory);
+ }
+
+ public boolean isSecureConnection() {
+ return true;
+ }
+
+ /**
+ * An SSL socket factory that will let any certifacte past, even if it's expired or
+ * not singed by a root CA.
+ */
+ private static class DummySSLSocketFactory extends SSLSocketFactory {
+
+ private SSLSocketFactory factory;
+
+ public DummySSLSocketFactory() {
+
+ try {
+ SSLContext sslcontent = SSLContext.getInstance("TLS");
+ sslcontent.init(null, // KeyManager not required
+ new TrustManager[] { new OpenTrustManager() },
+ new java.security.SecureRandom());
+ factory = sslcontent.getSocketFactory();
+ }
+ catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ catch (KeyManagementException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static SocketFactory getDefault() {
+ return new DummySSLSocketFactory();
+ }
+
+ public Socket createSocket(Socket socket, String s, int i, boolean flag)
+ throws IOException
+ {
+ return factory.createSocket(socket, s, i, flag);
+ }
+
+ public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr2, int j)
+ throws IOException
+ {
+ return factory.createSocket(inaddr, i, inaddr2, j);
+ }
+
+ public Socket createSocket(InetAddress inaddr, int i) throws IOException {
+ return factory.createSocket(inaddr, i);
+ }
+
+ public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
+ return factory.createSocket(s, i, inaddr, j);
+ }
+
+ public Socket createSocket(String s, int i) throws IOException {
+ return factory.createSocket(s, i);
+ }
+
+ public String[] getDefaultCipherSuites() {
+ return factory.getSupportedCipherSuites();
+ }
+
+ public String[] getSupportedCipherSuites() {
+ return factory.getSupportedCipherSuites();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ServerTrustManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ServerTrustManager.java
new file mode 100644
index 000000000..9e5fff001
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ServerTrustManager.java
@@ -0,0 +1,184 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2005 Jive Software.
+ *
+ * All rights reserved. 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 javax.net.ssl.X509TrustManager;
+import java.io.FileInputStream;
+import java.security.*;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+/**
+ * Trust manager that checks all certificates presented by the server. This class
+ * is used during TLS negotiation. It is possible to disable/enable some or all checkings
+ * by configuring the {@link ConnectionConfiguration}. The truststore file that contains
+ * knows and trusted CA root certificates can also be configure in {@link ConnectionConfiguration}.
+ *
+ * @author Gaston Dombiak
+ */
+class ServerTrustManager implements X509TrustManager {
+
+ private ConnectionConfiguration configuration;
+
+ /**
+ * Holds the domain of the remote server we are trying to connect
+ */
+ private String server;
+ private KeyStore trustStore;
+
+ public ServerTrustManager(String server, ConnectionConfiguration configuration) {
+ this.configuration = configuration;
+ this.server = server;
+
+ try {
+ trustStore = KeyStore.getInstance(configuration.getTruststoreType());
+ trustStore.load(new FileInputStream(configuration.getTruststorePath()),
+ configuration.getTruststorePassword().toCharArray());
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ // Disable root CA checking
+ configuration.setVerifyRootCAEnabled(false);
+ }
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ public void checkClientTrusted(X509Certificate[] arg0, String arg1)
+ throws CertificateException {
+ }
+
+ public void checkServerTrusted(X509Certificate[] x509Certificates, String arg1)
+ throws CertificateException {
+
+ int nSize = x509Certificates.length;
+
+ String peerIdentity = getPeerIdentity(x509Certificates[0]);
+
+ if (configuration.isVerifyChainEnabled()) {
+ // Working down the chain, for every certificate in the chain,
+ // verify that the subject of the certificate is the issuer of the
+ // next certificate in the chain.
+ Principal principalLast = null;
+ for (int i = nSize -1; i >= 0 ; i--) {
+ X509Certificate x509certificate = x509Certificates[i];
+ Principal principalIssuer = x509certificate.getIssuerDN();
+ Principal principalSubject = x509certificate.getSubjectDN();
+ if (principalLast != null) {
+ if (principalIssuer.equals(principalLast)) {
+ try {
+ PublicKey publickey =
+ x509Certificates[i + 1].getPublicKey();
+ x509Certificates[i].verify(publickey);
+ }
+ catch (GeneralSecurityException generalsecurityexception) {
+ throw new CertificateException(
+ "signature verification failed of " + peerIdentity);
+ }
+ }
+ else {
+ throw new CertificateException(
+ "subject/issuer verification failed of " + peerIdentity);
+ }
+ }
+ principalLast = principalSubject;
+ }
+ }
+
+ if (configuration.isVerifyRootCAEnabled()) {
+ // Verify that the the last certificate in the chain was issued
+ // by a third-party that the client trusts.
+ boolean trusted = false;
+ try {
+ trusted = trustStore.getCertificateAlias(x509Certificates[nSize - 1]) != null;
+ if (!trusted && nSize == 1 && configuration.isSelfSignedCertificateEnabled())
+ {
+ System.out.println("Accepting self-signed certificate of remote server: " +
+ peerIdentity);
+ trusted = true;
+ }
+ }
+ catch (KeyStoreException e) {
+ e.printStackTrace();
+ }
+ if (!trusted) {
+ throw new CertificateException("root certificate not trusted of " + peerIdentity);
+ }
+ }
+
+ if (configuration.isNotMatchingDomainCheckEnabled()) {
+ // Verify that the first certificate in the chain corresponds to
+ // the server we desire to authenticate.
+ // Check if the certificate uses a wildcard indicating that subdomains are valid
+ if (peerIdentity.startsWith("*.")) {
+ // Remove the wildcard
+ peerIdentity = peerIdentity.substring(2);
+ // Check if the requested subdomain matches the certified domain
+ if (!server.endsWith(peerIdentity)) {
+ throw new CertificateException("target verification failed of " + peerIdentity);
+ }
+ }
+ else if (!server.equals(peerIdentity)) {
+ throw new CertificateException("target verification failed of " + peerIdentity);
+ }
+ }
+
+ if (configuration.isExpiredCertificatesCheckEnabled()) {
+ // For every certificate in the chain, verify that the certificate
+ // is valid at the current time.
+ Date date = new Date();
+ for (int i = 0; i < nSize; i++) {
+ try {
+ x509Certificates[i].checkValidity(date);
+ }
+ catch (GeneralSecurityException generalsecurityexception) {
+ throw new CertificateException("invalid date of " + server);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Returns the identity of the remote server as defined in the specified certificate. The
+ * identity is defined in the subjectDN of the certificate and it can also be defined in
+ * the subjectAltName extension of type "xmpp". When the extension is being used then the
+ * identity defined in the extension in going to be returned. Otherwise, the value stored in
+ * the subjectDN is returned.
+ *
+ * @param x509Certificate the certificate the holds the identity of the remote server.
+ * @return the identity of the remote server as defined in the specified certificate.
+ */
+ public static String getPeerIdentity(X509Certificate x509Certificate) {
+ Principal principalSubject = x509Certificate.getSubjectDN();
+ // TODO Look the identity in the subjectAltName extension if available
+ String name = principalSubject.getName();
+ if (name.startsWith("CN=")) {
+ // Remove the CN= prefix
+ name = name.substring(3);
+ }
+ return name;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SmackConfiguration.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SmackConfiguration.java
new file mode 100644
index 000000000..250f372d6
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SmackConfiguration.java
@@ -0,0 +1,207 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.xmlpull.mxp1.MXParser;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+
+/**
+ * Represents the configuration of Smack. The configuration is used for:
+ * <ul>
+ * <li> Initializing classes by loading them at start-up.
+ * <li> Getting the current Smack version.
+ * <li> Getting and setting global library behavior, such as the period of time
+ * to wait for replies to packets from the server. Note: setting these values
+ * via the API will override settings in the configuration file.
+ * </ul>
+ *
+ * Configuration settings are stored in META-INF/smack-config.xml (typically inside the
+ * smack.jar file).
+ *
+ * @author Gaston Dombiak
+ */
+public final class SmackConfiguration {
+
+ private static final String SMACK_VERSION = "2.2.0";
+
+ private static int packetReplyTimeout = 5000;
+ private static int keepAliveInterval = 30000;
+
+ private SmackConfiguration() {
+ }
+
+ /**
+ * Loads the configuration from the smack-config.xml file.<p>
+ *
+ * So far this means that:
+ * 1) a set of classes will be loaded in order to execute their static init block
+ * 2) retrieve and set the current Smack release
+ */
+ static {
+ try {
+ // Get an array of class loaders to try loading the providers files from.
+ ClassLoader[] classLoaders = getClassLoaders();
+ for (int i = 0; i < classLoaders.length; i++) {
+ Enumeration configEnum = classLoaders[i].getResources("META-INF/smack-config.xml");
+ while (configEnum.hasMoreElements()) {
+ URL url = (URL) configEnum.nextElement();
+ InputStream systemStream = null;
+ try {
+ systemStream = url.openStream();
+ XmlPullParser parser = new MXParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ parser.setInput(systemStream, "UTF-8");
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("className")) {
+ // Attempt to load the class so that the class can get initialized
+ parseClassToLoad(parser);
+ }
+ else if (parser.getName().equals("packetReplyTimeout")) {
+ packetReplyTimeout = parseIntProperty(parser, packetReplyTimeout);
+ }
+ else if (parser.getName().equals("keepAliveInterval")) {
+ keepAliveInterval = parseIntProperty(parser, keepAliveInterval);
+ }
+ }
+ eventType = parser.next();
+ }
+ while (eventType != XmlPullParser.END_DOCUMENT);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ finally {
+ try {
+ systemStream.close();
+ }
+ catch (Exception e) {
+ }
+ }
+ }
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Returns the Smack version information, eg "1.3.0".
+ *
+ * @return the Smack version information.
+ */
+ public static String getVersion() {
+ return SMACK_VERSION;
+ }
+
+ /**
+ * Returns the number of milliseconds to wait for a response from
+ * the server. The default value is 5000 ms.
+ *
+ * @return the milliseconds to wait for a response from the server
+ */
+ public static int getPacketReplyTimeout() {
+ // The timeout value must be greater than 0 otherwise we will answer the default value
+ if (packetReplyTimeout <= 0) {
+ packetReplyTimeout = 5000;
+ }
+ return packetReplyTimeout;
+ }
+
+ /**
+ * Sets the number of milliseconds to wait for a response from
+ * the server.
+ *
+ * @param timeout the milliseconds to wait for a response from the server
+ */
+ public static void setPacketReplyTimeout(int timeout) {
+ if (timeout <= 0) {
+ throw new IllegalArgumentException();
+ }
+ packetReplyTimeout = timeout;
+ }
+
+ /**
+ * Returns the number of milleseconds delay between sending keep-alive
+ * requests to the server. The default value is 30000 ms. A value of -1
+ * mean no keep-alive requests will be sent to the server.
+ *
+ * @return the milliseconds to wait between keep-alive requests, or -1 if
+ * no keep-alive should be sent.
+ */
+ public static int getKeepAliveInterval() {
+ return keepAliveInterval;
+ }
+
+ /**
+ * Sets the number of milleseconds delay between sending keep-alive
+ * requests to the server. The default value is 30000 ms. A value of -1
+ * mean no keep-alive requests will be sent to the server.
+ *
+ * @param interval the milliseconds to wait between keep-alive requests,
+ * or -1 if no keep-alive should be sent.
+ */
+ public static void setKeepAliveInterval(int interval) {
+ keepAliveInterval = interval;
+ }
+
+ private static void parseClassToLoad(XmlPullParser parser) throws Exception {
+ String className = parser.nextText();
+ // Attempt to load the class so that the class can get initialized
+ try {
+ Class.forName(className);
+ }
+ catch (ClassNotFoundException cnfe) {
+ System.err.println("Error! A startup class specified in smack-config.xml could " +
+ "not be loaded: " + className);
+ }
+ }
+
+ private static int parseIntProperty(XmlPullParser parser, int defaultValue)
+ throws Exception
+ {
+ try {
+ return Integer.parseInt(parser.nextText());
+ }
+ catch (NumberFormatException nfe) {
+ nfe.printStackTrace();
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns an array of class loaders to load resources from.
+ *
+ * @return an array of ClassLoader instances.
+ */
+ private static ClassLoader[] getClassLoaders() {
+ ClassLoader[] classLoaders = new ClassLoader[2];
+ classLoaders[0] = new SmackConfiguration().getClass().getClassLoader();
+ classLoaders[1] = Thread.currentThread().getContextClassLoader();
+ return classLoaders;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/UserAuthentication.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/UserAuthentication.java
new file mode 100644
index 000000000..2d3a319ac
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/UserAuthentication.java
@@ -0,0 +1,55 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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;
+
+/**
+ * There are two ways to authenticate a user with a server. Using SASL or Non-SASL
+ * authentication. This interface makes {@link SASLAuthentication} and
+ * {@link NonSASLAuthentication} polyphormic.
+ *
+ * @author Gaston Dombiak
+ */
+interface UserAuthentication {
+
+ /**
+ * Authenticates the user with the server. This method will return the full JID provided by
+ * the server. The server may assign a full JID with a username and resource different than
+ * the requested by this method.
+ *
+ * @param username the username that is authenticating with the server.
+ * @param password the password to send to the server.
+ * @param resource the desired resource.
+ * @return the full JID provided by the server while binding a resource for the connection.
+ * @throws XMPPException if an error occures while authenticating.
+ */
+ String authenticate(String username, String password, String resource) throws
+ XMPPException;
+
+ /**
+ * Performs an anonymous authentication with the server. The server will created a new full JID
+ * for this connection. An exception will be thrown if the server does not support anonymous
+ * authentication.
+ *
+ * @return the full JID provided by the server while binding a resource for the connection.
+ * @throws XMPPException if an error occures while authenticating.
+ */
+ String authenticateAnonymously() throws XMPPException;
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPConnection.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPConnection.java
new file mode 100644
index 000000000..ce5be5c5f
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPConnection.java
@@ -0,0 +1,1249 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.debugger.SmackDebugger;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.StringUtils;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import java.io.*;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.*;
+
+/**
+ * Creates a connection to a XMPP server. A simple use of this API might
+ * look like the following:
+ * <pre>
+ * // Create a connection to the jivesoftware.com XMPP server.
+ * XMPPConnection con = new XMPPConnection("jivesoftware.com");
+ * // Most servers require you to login before performing other tasks.
+ * con.login("jsmith", "mypass");
+ * // Start a new conversation with John Doe and send him a message.
+ * Chat chat = con.createChat("jdoe@jabber.org");
+ * chat.sendMessage("Hey, how's it going?");
+ * </pre>
+ *
+ * @author Matt Tucker
+ */
+public class XMPPConnection {
+
+ /**
+ * Value that indicates whether debugging is enabled. When enabled, a debug
+ * window will apear for each new connection that will contain the following
+ * information:<ul>
+ * <li> Client Traffic -- raw XML traffic generated by Smack and sent to the server.
+ * <li> Server Traffic -- raw XML traffic sent by the server to the client.
+ * <li> Interpreted Packets -- shows XML packets from the server as parsed by Smack.
+ * </ul>
+ *
+ * Debugging can be enabled by setting this field to true, or by setting the Java system
+ * property <tt>smack.debugEnabled</tt> to true. The system property can be set on the
+ * command line such as "java SomeApp -Dsmack.debugEnabled=true".
+ */
+ public static boolean DEBUG_ENABLED = false;
+
+ private static List connectionEstablishedListeners = new ArrayList();
+
+ static {
+ // Use try block since we may not have permission to get a system
+ // property (for example, when an applet).
+ try {
+ DEBUG_ENABLED = Boolean.getBoolean("smack.debugEnabled");
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+ // Ensure the SmackConfiguration class is loaded by calling a method in it.
+ SmackConfiguration.getVersion();
+ }
+ private SmackDebugger debugger = null;
+
+ /**
+ * IP address or host name of the server. This information is only used when
+ * creating new socket connections to the server. If this information is not
+ * configured then it will be assumed that the host name matches the service name.
+ */
+ String host;
+ int port;
+ Socket socket;
+
+ /**
+ * Hostname of the XMPP server. Usually servers use the same service name as the name
+ * of the server. However, there are some servers like google where host would be
+ * talk.google.com and the serviceName would be gmail.com.
+ */
+ String serviceName;
+
+ String connectionID;
+ private String user = null;
+ private boolean connected = false;
+ private boolean authenticated = false;
+ private boolean anonymous = false;
+ private boolean usingTLS = false;
+
+ PacketWriter packetWriter;
+ PacketReader packetReader;
+
+ Roster roster = null;
+ private AccountManager accountManager = null;
+ private SASLAuthentication saslAuthentication = new SASLAuthentication(this);
+
+ Writer writer;
+ Reader reader;
+
+ /**
+ * A map between JIDs and the most recently created Chat object with that JID.
+ * Reference to the Chat is stored via a WeakReference so that the map
+ * does not interfere with garbage collection. The map of chats must be stored
+ * with each connection.
+ */
+ Map chats = Collections.synchronizedMap(new HashMap());
+
+ /**
+ * Collection of available stream compression methods offered by the server.
+ */
+ private Collection compressionMethods;
+ /**
+ * Flag that indicates if stream compression is actually in use.
+ */
+ private boolean usingCompression;
+ /**
+ * Holds the initial configuration used while creating the connection.
+ */
+ private ConnectionConfiguration configuration;
+
+ /**
+ * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be
+ * performed to try to determine the IP address and port corresponding to the
+ * serviceName; if that lookup fails, it's assumed that server resides at serviceName
+ * with the default port of 5222. This is the preferred constructor for connecting
+ * to an XMPP server.
+ *
+ * @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>.
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropiate error messages to end-users.
+ */
+ public XMPPConnection(String serviceName) throws XMPPException {
+ // Perform DNS lookup to get host and port to use
+ DNSUtil.HostAddress address = DNSUtil.resolveXMPPDomain(serviceName);
+ // Create the configuration for this new connection
+ ConnectionConfiguration config =
+ new ConnectionConfiguration(address.getHost(), address.getPort(), serviceName);
+ config.setTLSEnabled(true);
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ // Set the new connection configuration
+ connectUsingConfiguration(config, null);
+ }
+
+ /**
+ * Creates a new connection to the XMPP server at the specifiec host and port.
+ *
+ * @param host the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>.
+ * @param port the port on the server that should be used; e.g. <tt>5222</tt>.
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropiate error messages to end-users.
+ */
+ public XMPPConnection(String host, int port) throws XMPPException {
+ // Create the configuration for this new connection
+ ConnectionConfiguration config = new ConnectionConfiguration(host, port);
+ config.setTLSEnabled(true);
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ // Set the new connection configuration
+ connectUsingConfiguration(config, null);
+ }
+
+ /**
+ * Creates a new connection to the specified XMPP server on the given host and port.
+ *
+ * @param host the host name, or null for the loopback address.
+ * @param port the port on the server that should be used; e.g. <tt>5222</tt>.
+ * @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>.
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropiate error messages to end-users.
+ */
+ public XMPPConnection(String host, int port, String serviceName) throws XMPPException {
+ // Create the configuration for this new connection
+ ConnectionConfiguration config = new ConnectionConfiguration(host, port, serviceName);
+ config.setTLSEnabled(true);
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ // Set the new connection configuration
+ connectUsingConfiguration(config, null);
+ }
+
+ /**
+ * Creates a new connection to the specified XMPP server on the given port using the
+ * specified SocketFactory.<p>
+ *
+ * A custom SocketFactory allows fine-grained control of the actual connection to the
+ * XMPP server. A typical use for a custom SocketFactory is when connecting through a
+ * SOCKS proxy.
+ *
+ * @param host the host name, or null for the loopback address.
+ * @param port the port on the server that should be used; e.g. <tt>5222</tt>.
+ * @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>.
+ * @param socketFactory a SocketFactory that will be used to create the socket to the XMPP
+ * server.
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropiate error messages to end-users.
+ */
+ public XMPPConnection(String host, int port, String serviceName, SocketFactory socketFactory)
+ throws XMPPException
+ {
+ // Create the configuration for this new connection
+ ConnectionConfiguration config = new ConnectionConfiguration(host, port, serviceName);
+ config.setTLSEnabled(true);
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ // Set the new connection configuration
+ connectUsingConfiguration(config, socketFactory);
+ }
+
+ public XMPPConnection(ConnectionConfiguration config) throws XMPPException {
+ // Set the new connection configuration
+ connectUsingConfiguration(config, null);
+ }
+
+ public XMPPConnection(ConnectionConfiguration config, SocketFactory socketFactory)
+ throws XMPPException {
+ // Set the new connection configuration
+ connectUsingConfiguration(config, socketFactory);
+ }
+
+ private void connectUsingConfiguration(ConnectionConfiguration config,
+ SocketFactory socketFactory) throws XMPPException {
+ this.host = config.getHost();
+ this.port = config.getPort();
+ try {
+ if (socketFactory == null) {
+ this.socket = new Socket(host, port);
+ }
+ else {
+ this.socket = socketFactory.createSocket(host, port);
+ }
+ }
+ catch (UnknownHostException uhe) {
+ throw new XMPPException(
+ "Could not connect to " + host + ":" + port + ".",
+ new XMPPError(504),
+ uhe);
+ }
+ catch (IOException ioe) {
+ throw new XMPPException(
+ "XMPPError connecting to " + host + ":" + port + ".",
+ new XMPPError(502),
+ ioe);
+ }
+ this.serviceName = config.getServiceName();
+ try {
+ // Keep a copy to be sure that once the configuration has been passed to the
+ // constructor it cannot be modified
+ this.configuration = (ConnectionConfiguration) config.clone();
+ }
+ catch (CloneNotSupportedException e) {}
+ init();
+ }
+
+ /**
+ * Package-private default constructor. This constructor is only intended
+ * for unit testing. Normal classes extending XMPPConnection should override
+ * one of the other constructors.
+ */
+ XMPPConnection() {
+ }
+
+ /**
+ * Returns the connection ID for this connection, which is the value set by the server
+ * when opening a XMPP stream. If the server does not set a connection ID, this value
+ * will be null.
+ *
+ * @return the ID of this connection returned from the XMPP server.
+ */
+ public String getConnectionID() {
+ return connectionID;
+ }
+
+ /**
+ * Returns the name of the service provided by the XMPP server for this connection. After
+ * authenticating with the server the returned value may be different.
+ *
+ * @return the name of the service provided by the XMPP server.
+ */
+ public String getServiceName() {
+ return serviceName;
+ }
+
+ /**
+ * Returns the host name of the server where the XMPP server is running. This would be the
+ * IP address of the server or a name that may be resolved by a DNS server.
+ *
+ * @return the host name of the server where the XMPP server is running.
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * Returns the port number of the XMPP server for this connection. The default port
+ * for normal connections is 5222. The default port for SSL connections is 5223.
+ *
+ * @return the port number of the XMPP server.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Returns the full XMPP address of the user that is logged in to the connection or
+ * <tt>null</tt> if not logged in yet. An XMPP address is in the form
+ * username@server/resource.
+ *
+ * @return the full XMPP address of the user logged in.
+ */
+ public String getUser() {
+ if (!isAuthenticated()) {
+ return null;
+ }
+ return user;
+ }
+
+ /**
+ * Logs in to the server using the strongest authentication mode supported by
+ * the server, then set our presence to available. If more than five seconds
+ * (default timeout) elapses in each step of the authentication process without
+ * a response from the server, or if an error occurs, a XMPPException will be thrown.
+ *
+ * @param username the username.
+ * @param password the password.
+ * @throws XMPPException if an error occurs.
+ */
+ public void login(String username, String password) throws XMPPException {
+ login(username, password, "Smack");
+ }
+
+ /**
+ * Logs in to the server using the strongest authentication mode supported by
+ * the server, then sets presence to available. If more than five seconds
+ * (default timeout) elapses in each step of the authentication process without
+ * a response from the server, or if an error occurs, a XMPPException will be thrown.
+ *
+ * @param username the username.
+ * @param password the password.
+ * @param resource the resource.
+ * @throws XMPPException if an error occurs.
+ * @throws IllegalStateException if not connected to the server, or already logged in
+ * to the serrver.
+ */
+ public synchronized void login(String username, String password, String resource)
+ throws XMPPException
+ {
+ login(username, password, resource, true);
+ }
+
+ /**
+ * Logs in to the server using the strongest authentication mode supported by
+ * the server. If the server supports SASL authentication then the user will be
+ * authenticated using SASL if not Non-SASL authentication will be tried. An available
+ * presence may optionally be sent. If <tt>sendPresence</tt>
+ * is false, a presence packet must be sent manually later. If more than five seconds
+ * (default timeout) elapses in each step of the authentication process without a
+ * response from the server, or if an error occurs, a XMPPException will be thrown.
+ *
+ * @param username the username.
+ * @param password the password.
+ * @param resource the resource.
+ * @param sendPresence if <tt>true</tt> an available presence will be sent automatically
+ * after login is completed.
+ * @throws XMPPException if an error occurs.
+ * @throws IllegalStateException if not connected to the server, or already logged in
+ * to the serrver.
+ */
+ public synchronized void login(String username, String password, String resource,
+ boolean sendPresence) throws XMPPException
+ {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ if (authenticated) {
+ throw new IllegalStateException("Already logged in to server.");
+ }
+ // Do partial version of nameprep on the username.
+ username = username.toLowerCase().trim();
+
+ String response = null;
+ if (configuration.isSASLAuthenticationEnabled() &&
+ saslAuthentication.hasNonAnonymousAuthentication()) {
+ // Authenticate using SASL
+ response = saslAuthentication.authenticate(username, password, resource);
+ }
+ else {
+ // Authenticate using Non-SASL
+ response = new NonSASLAuthentication(this).authenticate(username, password, resource);
+ }
+
+ // Set the user.
+ if (response != null) {
+ this.user = response;
+ // Update the serviceName with the one returned by the server
+ this.serviceName = StringUtils.parseServer(response);
+ }
+ else {
+ this.user = username + "@" + this.serviceName;
+ if (resource != null) {
+ this.user += "/" + resource;
+ }
+ }
+
+ // If compression is enabled then request the server to use stream compression
+ if (configuration.isCompressionEnabled()) {
+ useCompression();
+ }
+
+ // Create the roster.
+ this.roster = new Roster(this);
+ roster.reload();
+
+ // Set presence to online.
+ if (sendPresence) {
+ packetWriter.sendPacket(new Presence(Presence.Type.AVAILABLE));
+ }
+
+ // Indicate that we're now authenticated.
+ authenticated = true;
+
+ packetReader.notifyConnectionAuthenticated();
+
+ anonymous = false;
+
+ // If debugging is enabled, change the the debug window title to include the
+ // name we are now logged-in as.
+ // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
+ // will be null
+ if (configuration.isDebuggerEnabled() && debugger != null) {
+ debugger.userHasLogged(user);
+ }
+ }
+
+ /**
+ * Logs in to the server anonymously. Very few servers are configured to support anonymous
+ * authentication, so it's fairly likely logging in anonymously will fail. If anonymous login
+ * does succeed, your XMPP address will likely be in the form "server/123ABC" (where "123ABC"
+ * is a random value generated by the server).
+ *
+ * @throws XMPPException if an error occurs or anonymous logins are not supported by the server.
+ * @throws IllegalStateException if not connected to the server, or already logged in
+ * to the serrver.
+ */
+ public synchronized void loginAnonymously() throws XMPPException {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ if (authenticated) {
+ throw new IllegalStateException("Already logged in to server.");
+ }
+
+ String response = null;
+ if (configuration.isSASLAuthenticationEnabled() &&
+ saslAuthentication.hasAnonymousAuthentication()) {
+ response = saslAuthentication.authenticateAnonymously();
+ }
+ else {
+ // Authenticate using Non-SASL
+ response = new NonSASLAuthentication(this).authenticateAnonymously();
+ }
+
+ // Set the user value.
+ this.user = response;
+ // Update the serviceName with the one returned by the server
+ this.serviceName = StringUtils.parseServer(response);
+
+ // If compression is enabled then request the server to use stream compression
+ if (configuration.isCompressionEnabled()) {
+ useCompression();
+ }
+
+ // Anonymous users can't have a roster.
+ roster = null;
+
+ // Set presence to online.
+ packetWriter.sendPacket(new Presence(Presence.Type.AVAILABLE));
+
+ // Indicate that we're now authenticated.
+ authenticated = true;
+
+ packetReader.notifyConnectionAuthenticated();
+
+ anonymous = true;
+
+ // If debugging is enabled, change the the debug window title to include the
+ // name we are now logged-in as.
+ // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
+ // will be null
+ if (configuration.isDebuggerEnabled() && debugger != null) {
+ debugger.userHasLogged(user);
+ }
+ }
+
+ /**
+ * Returns the roster for the user logged into the server. If the user has not yet
+ * logged into the server (or if the user is logged in anonymously), this method will return
+ * <tt>null</tt>.
+ *
+ * @return the user's roster, or <tt>null</tt> if the user has not logged in yet.
+ */
+ public Roster getRoster() {
+ if (roster == null) {
+ return null;
+ }
+ // If this is the first time the user has asked for the roster after calling
+ // login, we want to wait for the server to send back the user's roster. This
+ // behavior shields API users from having to worry about the fact that roster
+ // operations are asynchronous, although they'll still have to listen for
+ // changes to the roster. Note: because of this waiting logic, internal
+ // Smack code should be wary about calling the getRoster method, and may need to
+ // access the roster object directly.
+ if (!roster.rosterInitialized) {
+ try {
+ synchronized (roster) {
+ long waitTime = SmackConfiguration.getPacketReplyTimeout();
+ long start = System.currentTimeMillis();
+ while (!roster.rosterInitialized) {
+ if (waitTime <= 0) {
+ break;
+ }
+ roster.wait(waitTime);
+ long now = System.currentTimeMillis();
+ waitTime -= now - start;
+ start = now;
+ }
+ }
+ }
+ catch (InterruptedException ie) {
+ // Ignore.
+ }
+ }
+ return roster;
+ }
+
+ /**
+ * Returns an account manager instance for this connection.
+ *
+ * @return an account manager for this connection.
+ */
+ public synchronized AccountManager getAccountManager() {
+ if (accountManager == null) {
+ accountManager = new AccountManager(this);
+ }
+ return accountManager;
+ }
+
+ /**
+ * Creates a new chat with the specified participant. The participant should
+ * be a valid XMPP user such as <tt>jdoe@jivesoftware.com</tt> or
+ * <tt>jdoe@jivesoftware.com/work</tt>.
+ *
+ * @param participant the person to start the conversation with.
+ * @return a new Chat object.
+ */
+ public Chat createChat(String participant) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ return new Chat(this, participant);
+ }
+
+ /**
+ * Creates a new group chat connected to the specified room. The room name
+ * should be full address, such as <tt>room@chat.example.com</tt>.
+ * <p>
+ * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
+ * for the XMPP server example.com). You must ensure that the room address you're
+ * trying to connect to includes the proper chat sub-domain.
+ *
+ * @param room the fully qualifed name of the room.
+ * @return a new GroupChat object.
+ */
+ public GroupChat createGroupChat(String room) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ return new GroupChat(this, room);
+ }
+
+ /**
+ * Returns true if currently connected to the XMPP server.
+ *
+ * @return true if connected.
+ */
+ public boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ * Returns true if the connection is a secured one, such as an SSL connection or
+ * if TLS was negotiated successfully.
+ *
+ * @return true if a secure connection to the server.
+ */
+ public boolean isSecureConnection() {
+ return isUsingTLS();
+ }
+
+ /**
+ * Returns true if currently authenticated by successfully calling the login method.
+ *
+ * @return true if authenticated.
+ */
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ /**
+ * Returns true if currently authenticated anonymously.
+ *
+ * @return true if authenticated anonymously.
+ */
+ public boolean isAnonymous() {
+ return anonymous;
+ }
+
+ /**
+ * Closes the connection by setting presence to unavailable then closing the stream to
+ * the XMPP server. Once a connection has been closed, it cannot be re-opened.
+ */
+ public void close() {
+ // Set presence to offline.
+ packetWriter.sendPacket(new Presence(Presence.Type.UNAVAILABLE));
+ packetReader.shutdown();
+ packetWriter.shutdown();
+ // Wait 150 ms for processes to clean-up, then shutdown.
+ try {
+ Thread.sleep(150);
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+
+ // Close down the readers and writers.
+ if (reader != null)
+ {
+ try { reader.close(); } catch (Throwable ignore) { /* ignore */ }
+ reader = null;
+ }
+ if (writer != null)
+ {
+ try { writer.close(); } catch (Throwable ignore) { /* ignore */ }
+ writer = null;
+ }
+
+ try {
+ socket.close();
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+ authenticated = false;
+ connected = false;
+ }
+
+ /**
+ * Sends the specified packet to the server.
+ *
+ * @param packet the packet to send.
+ */
+ public void sendPacket(Packet packet) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ if (packet == null) {
+ throw new NullPointerException("Packet is null.");
+ }
+ packetWriter.sendPacket(packet);
+ }
+
+ /**
+ * Registers a packet listener with this connection. A packet filter determines
+ * which packets will be delivered to the listener.
+ *
+ * @param packetListener the packet listener to notify of new packets.
+ * @param packetFilter the packet filter to use.
+ */
+ public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ packetReader.addPacketListener(packetListener, packetFilter);
+ }
+
+ /**
+ * Removes a packet listener from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ */
+ public void removePacketListener(PacketListener packetListener) {
+ packetReader.removePacketListener(packetListener);
+ }
+
+ /**
+ * Registers a packet listener with this connection. The listener will be
+ * notified of every packet that this connection sends. A packet filter determines
+ * which packets will be delivered to the listener. Note that the thread
+ * that writes packets will be used to invoke the listeners. Therefore, each
+ * packet listener should complete all operations quickly or use a different
+ * thread for processing.
+ *
+ * @param packetListener the packet listener to notify of sent packets.
+ * @param packetFilter the packet filter to use.
+ */
+ public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ packetWriter.addPacketListener(packetListener, packetFilter);
+ }
+
+ /**
+ * Removes a packet listener from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ */
+ public void removePacketWriterListener(PacketListener packetListener) {
+ packetWriter.removePacketListener(packetListener);
+ }
+
+ /**
+ * Registers a packet interceptor with this connection. The interceptor will be
+ * invoked every time a packet is about to be sent by this connection. Interceptors
+ * may modify the packet to be sent. A packet filter determines which packets
+ * will be delivered to the interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to notify of packets about to be sent.
+ * @param packetFilter the packet filter to use.
+ */
+ public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor,
+ PacketFilter packetFilter) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ packetWriter.addPacketInterceptor(packetInterceptor, packetFilter);
+ }
+
+ /**
+ * Removes a packet interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to remove.
+ */
+ public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) {
+ packetWriter.removePacketInterceptor(packetInterceptor);
+ }
+
+ /**
+ * Creates a new packet collector for this connection. A packet filter determines
+ * which packets will be accumulated by the collector.
+ *
+ * @param packetFilter the packet filter to use.
+ * @return a new packet collector.
+ */
+ public PacketCollector createPacketCollector(PacketFilter packetFilter) {
+ return packetReader.createPacketCollector(packetFilter);
+ }
+
+ /**
+ * Adds a connection listener to this connection that will be notified when
+ * the connection closes or fails.
+ *
+ * @param connectionListener a connection listener.
+ */
+ public void addConnectionListener(ConnectionListener connectionListener) {
+ if (connectionListener == null) {
+ return;
+ }
+ synchronized (packetReader.connectionListeners) {
+ if (!packetReader.connectionListeners.contains(connectionListener)) {
+ packetReader.connectionListeners.add(connectionListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a connection listener from this connection.
+ *
+ * @param connectionListener a connection listener.
+ */
+ public void removeConnectionListener(ConnectionListener connectionListener) {
+ synchronized (packetReader.connectionListeners) {
+ packetReader.connectionListeners.remove(connectionListener);
+ }
+ }
+
+ /**
+ * Adds a connection established listener that will be notified when a new connection
+ * is established.
+ *
+ * @param connectionEstablishedListener a listener interested on connection established events.
+ */
+ public static void addConnectionListener(ConnectionEstablishedListener connectionEstablishedListener) {
+ synchronized (connectionEstablishedListeners) {
+ if (!connectionEstablishedListeners.contains(connectionEstablishedListener)) {
+ connectionEstablishedListeners.add(connectionEstablishedListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener on new established connections.
+ *
+ * @param connectionEstablishedListener a listener interested on connection established events.
+ */
+ public static void removeConnectionListener(ConnectionEstablishedListener connectionEstablishedListener) {
+ synchronized (connectionEstablishedListeners) {
+ connectionEstablishedListeners.remove(connectionEstablishedListener);
+ }
+ }
+
+ /**
+ * Initializes the connection by creating a packet reader and writer and opening a
+ * XMPP stream to the server.
+ *
+ * @throws XMPPException if establishing a connection to the server fails.
+ */
+ private void init() throws XMPPException {
+ // Set the reader and writer instance variables
+ initReaderAndWriter();
+
+ try
+ {
+ packetWriter = new PacketWriter(this);
+ packetReader = new PacketReader(this);
+
+ // If debugging is enabled, we should start the thread that will listen for
+ // all packets and then log them.
+ if (configuration.isDebuggerEnabled()) {
+ packetReader.addPacketListener(debugger.getReaderListener(), null);
+ if (debugger.getWriterListener() != null) {
+ packetWriter.addPacketListener(debugger.getWriterListener(), null);
+ }
+ }
+ // Start the packet writer. This will open a XMPP stream to the server
+ packetWriter.startup();
+ // Start the packet reader. The startup() method will block until we
+ // get an opening stream packet back from server.
+ packetReader.startup();
+
+ // Make note of the fact that we're now connected.
+ connected = true;
+
+ // Notify that a new connection has been established
+ connectionEstablished(this);
+
+ // Add a listener for all message packets so that we can deliver errant
+ // messages to the best Chat instance available.
+ addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ Message message = (Message)packet;
+ // Ignore any messages with a thread ID, as they will likely
+ // already be associated with a Chat. This will miss messages
+ // with new thread ID values, but we can only assume that a
+ // listener is registered to deal with this case.
+ if (message.getThread() == null &&
+ message.getType() != Message.Type.GROUP_CHAT &&
+ message.getType() != Message.Type.HEADLINE) {
+ WeakReference chatRef = (WeakReference)chats.get(
+ StringUtils.parseBareAddress(message.getFrom()));
+ if (chatRef != null) {
+ // Do some extra clean-up if the reference was cleared.
+ Chat chat;
+ if ((chat = (Chat)chatRef.get()) == null) {
+ chats.remove(message.getFrom());
+ }
+ else {
+ chat.deliver(message);
+ }
+ }
+ }
+ }
+ }, new PacketTypeFilter(Message.class));
+ }
+ catch (XMPPException ex) {
+ // An exception occurred in setting up the connection. Make sure we shut down the
+ // readers and writers and close the socket.
+
+ if (packetWriter != null) {
+ try { packetWriter.shutdown(); } catch (Throwable ignore) { /* ignore */ }
+ packetWriter = null;
+ }
+ if (packetReader != null) {
+ try { packetReader.shutdown(); } catch (Throwable ignore) { /* ignore */ }
+ packetReader = null;
+ }
+ if (reader != null) {
+ try { reader.close(); } catch (Throwable ignore) { /* ignore */ }
+ reader = null;
+ }
+ if (writer != null) {
+ try { writer.close(); } catch (Throwable ignore) { /* ignore */}
+ writer = null;
+ }
+ if (socket != null) {
+ try { socket.close(); } catch (Exception e) { /* ignore */ }
+ socket = null;
+ }
+ authenticated = false;
+ connected = false;
+
+ throw ex; // Everything stoppped. Now throw the exception.
+ }
+ }
+
+ private void initReaderAndWriter() throws XMPPException {
+ try {
+ if (!usingCompression) {
+ reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
+ writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
+ }
+ else {
+ try {
+ Class zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream");
+ //ZOutputStream out = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_COMPRESSION);
+ Constructor constructor =
+ zoClass.getConstructor(new Class[]{OutputStream.class, Integer.TYPE});
+ Object out = constructor.newInstance(new Object[] {socket.getOutputStream(), new Integer(9)});
+ //out.setFlushMode(JZlib.Z_PARTIAL_FLUSH);
+ Method method = zoClass.getMethod("setFlushMode", new Class[] {Integer.TYPE});
+ method.invoke(out, new Object[] {new Integer(1)});
+ writer = new BufferedWriter(new OutputStreamWriter((OutputStream) out, "UTF-8"));
+
+ Class ziClass = Class.forName("com.jcraft.jzlib.ZInputStream");
+ //ZInputStream in = new ZInputStream(socket.getInputStream());
+ constructor = ziClass.getConstructor(new Class[]{InputStream.class});
+ Object in = constructor.newInstance(new Object[] {socket.getInputStream()});
+ //in.setFlushMode(JZlib.Z_PARTIAL_FLUSH);
+ method = ziClass.getMethod("setFlushMode", new Class[] {Integer.TYPE});
+ method.invoke(in, new Object[] {new Integer(1)});
+ reader = new BufferedReader(new InputStreamReader((InputStream) in, "UTF-8"));
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
+ writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
+ }
+ }
+ }
+ catch (IOException ioe) {
+ throw new XMPPException(
+ "XMPPError establishing connection with server.",
+ new XMPPError(502),
+ ioe);
+ }
+
+ // If debugging is enabled, we open a window and write out all network traffic.
+ if (configuration.isDebuggerEnabled()) {
+ if (debugger == null) {
+ // Detect the debugger class to use.
+ String className = null;
+ // Use try block since we may not have permission to get a system
+ // property (for example, when an applet).
+ try {
+ className = System.getProperty("smack.debuggerClass");
+ }
+ catch (Throwable t) {
+ }
+ Class debuggerClass = null;
+ if (className != null) {
+ try {
+ debuggerClass = Class.forName(className);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ if (debuggerClass == null) {
+ try {
+ debuggerClass =
+ Class.forName("org.jivesoftware.smackx.debugger.EnhancedDebugger");
+ }
+ catch (Exception ex) {
+ try {
+ debuggerClass = Class.forName("org.jivesoftware.smack.debugger.LiteDebugger");
+ }
+ catch (Exception ex2) {
+ ex2.printStackTrace();
+ }
+ }
+ }
+ // Create a new debugger instance. If an exception occurs then disable the debugging
+ // option
+ try {
+ Constructor constructor =
+ debuggerClass.getConstructor(
+ new Class[] { XMPPConnection.class, Writer.class, Reader.class });
+ debugger = (SmackDebugger) constructor
+ .newInstance(new Object[]{this, writer, reader});
+ reader = debugger.getReader();
+ writer = debugger.getWriter();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ DEBUG_ENABLED = false;
+ }
+ }
+ else {
+ // Obtain new reader and writer from the existing debugger
+ reader = debugger.newConnectionReader(reader);
+ writer = debugger.newConnectionWriter(writer);
+ }
+ }
+ }
+
+ /**
+ * Fires listeners on connection established events.
+ */
+ private static void connectionEstablished(XMPPConnection connection) {
+ ConnectionEstablishedListener[] listeners = null;
+ synchronized (connectionEstablishedListeners) {
+ listeners = new ConnectionEstablishedListener[connectionEstablishedListeners.size()];
+ connectionEstablishedListeners.toArray(listeners);
+ }
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].connectionEstablished(connection);
+ }
+ }
+
+ /***********************************************
+ * TLS code below
+ **********************************************/
+
+ /**
+ * Returns true if the connection to the server has successfully negotiated TLS. Once TLS
+ * has been negotiatied the connection has been secured.
+ *
+ * @return true if the connection to the server has successfully negotiated TLS.
+ */
+ public boolean isUsingTLS() {
+ return usingTLS;
+ }
+
+ /**
+ * Returns the SASLAuthentication manager that is responsible for authenticating with
+ * the server.
+ *
+ * @return the SASLAuthentication manager that is responsible for authenticating with
+ * the server.
+ */
+ public SASLAuthentication getSASLAuthentication() {
+ return saslAuthentication;
+ }
+
+ /**
+ * Notification message saying that the server supports TLS so confirm the server that we
+ * want to secure the connection.
+ */
+ void startTLSReceived() {
+ if (!configuration.isTLSEnabled()) {
+ // Do not secure the connection using TLS since TLS was disabled
+ return;
+ }
+ try {
+ writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
+ writer.flush();
+ }
+ catch (IOException e) {
+ packetReader.notifyConnectionError(e);
+ }
+ }
+
+ /**
+ * The server has indicated that TLS negotiation can start. We now need to secure the
+ * existing plain connection and perform a handshake. This method won't return until the
+ * connection has finished the handshake or an error occured while securing the connection.
+ */
+ void proceedTLSReceived() throws Exception {
+ SSLContext context = SSLContext.getInstance("TLS");
+ // Verify certificate presented by the server
+ context.init(null, // KeyManager not required
+ new javax.net.ssl.TrustManager[]{new ServerTrustManager(serviceName, configuration)},
+ new java.security.SecureRandom());
+ Socket plain = socket;
+ // Secure the plain connection
+ socket = context.getSocketFactory().createSocket(plain,
+ plain.getInetAddress().getHostName(), plain.getPort(), true);
+ socket.setSoTimeout(0);
+ socket.setKeepAlive(true);
+ // Initialize the reader and writer with the new secured version
+ initReaderAndWriter();
+ // Proceed to do the handshake
+ ((SSLSocket) socket).startHandshake();
+
+ // Set that TLS was successful
+ usingTLS = true;
+
+ // Set the new writer to use
+ packetWriter.setWriter(writer);
+ // Send a new opening stream to the server
+ packetWriter.openStream();
+ }
+
+ /**
+ * Sets the available stream compression methods offered by the server.
+ *
+ * @param methods compression methods offered by the server.
+ */
+ void setAvailableCompressionMethods(Collection methods) {
+ compressionMethods = methods;
+ }
+
+ /**
+ * Returns true if the specified compression method was offered by the server.
+ *
+ * @param method the method to check.
+ * @return true if the specified compression method was offered by the server.
+ */
+ private boolean hasAvailableCompressionMethod(String method) {
+ return compressionMethods != null && compressionMethods.contains(method);
+ }
+
+ /**
+ * Returns true if network traffic is being compressed. When using stream compression network
+ * traffic can be reduced up to 90%. Therefore, stream compression is ideal when using a slow
+ * speed network connection. However, the server will need to use more CPU time in order to
+ * un/compress network data so under high load the server performance might be affected.<p>
+ *
+ * Note: To use stream compression the smackx.jar file has to be present in the classpath.
+ *
+ * @return true if network traffic is being compressed.
+ */
+ public boolean isUsingCompression() {
+ return usingCompression;
+ }
+
+ /**
+ * Starts using stream compression that will compress network traffic. Traffic can be
+ * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network
+ * connection. However, the server and the client will need to use more CPU time in order to
+ * un/compress network data so under high load the server performance might be affected.<p>
+ *
+ * Stream compression has to have been previously offered by the server. Currently only the
+ * zlib method is supported by the client. Stream compression negotiation has to be done
+ * before authentication took place.<p>
+ *
+ * Note: To use stream compression the smackx.jar file has to be present in the classpath.
+ *
+ * @return true if stream compression negotiation was successful.
+ */
+ private boolean useCompression() {
+ // If stream compression was offered by the server and we want to use
+ // compression then send compression request to the server
+ if (authenticated) {
+ throw new IllegalStateException("Compression should be negotiated before authentication.");
+ }
+ try {
+ Class.forName("com.jcraft.jzlib.ZOutputStream");
+ }
+ catch (ClassNotFoundException e) {
+ throw new IllegalStateException("Cannot use compression. Add smackx.jar to the classpath");
+ }
+ if (hasAvailableCompressionMethod("zlib")) {
+ requestStreamCompression();
+ // Wait until compression is being used or a timeout happened
+ synchronized (this) {
+ try {
+ this.wait(SmackConfiguration.getPacketReplyTimeout() * 5);
+ }
+ catch (InterruptedException e) {}
+ }
+ return usingCompression;
+ }
+ return false;
+ }
+
+ /**
+ * Request the server that we want to start using stream compression. When using TLS
+ * then negotiation of stream compression can only happen after TLS was negotiated. If TLS
+ * compression is being used the stream compression should not be used.
+ */
+ private void requestStreamCompression() {
+ try {
+ writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
+ writer.write("<method>zlib</method></compress>");
+ writer.flush();
+ }
+ catch (IOException e) {
+ packetReader.notifyConnectionError(e);
+ }
+ }
+
+ /**
+ * Start using stream compression since the server has acknowledged stream compression.
+ */
+ void startStreamCompression() throws Exception {
+ // Secure the plain connection
+ usingCompression = true;
+ // Initialize the reader and writer with the new secured version
+ initReaderAndWriter();
+
+ // Set the new writer to use
+ packetWriter.setWriter(writer);
+ // Send a new opening stream to the server
+ packetWriter.openStream();
+ // Notify that compression is being used
+ synchronized (this) {
+ this.notify();
+ }
+ }
+
+ void streamCompressionDenied() {
+ // Notify that compression has been denied
+ synchronized (this) {
+ this.notify();
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPException.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPException.java
new file mode 100644
index 000000000..5b60220ba
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPException.java
@@ -0,0 +1,219 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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 org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.packet.StreamError;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * A generic exception that is thrown when an error occurs performing an
+ * XMPP operation. XMPP servers can respond to error conditions with an error code
+ * and textual description of the problem, which are encapsulated in the XMPPError
+ * class. When appropriate, an XMPPError instance is attached instances of this exception.<p>
+ *
+ * When a stream error occured, the server will send a stream error to the client before
+ * closing the connection. Stream errors are unrecoverable errors. When a stream error
+ * is sent to the client an XMPPException will be thrown containing the StreamError sent
+ * by the server.
+ *
+ * @see XMPPError
+ * @author Matt Tucker
+ */
+public class XMPPException extends Exception {
+
+ private StreamError streamError = null;
+ private XMPPError error = null;
+ private Throwable wrappedThrowable = null;
+
+ /**
+ * Creates a new XMPPException.
+ */
+ public XMPPException() {
+ super();
+ }
+
+ /**
+ * Creates a new XMPPException with a description of the exception.
+ *
+ * @param message description of the exception.
+ */
+ public XMPPException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new XMPPException with the Throwable that was the root cause of the
+ * exception.
+ *
+ * @param wrappedThrowable the root cause of the exception.
+ */
+ public XMPPException(Throwable wrappedThrowable) {
+ super();
+ this.wrappedThrowable = wrappedThrowable;
+ }
+
+ /**
+ * Cretaes a new XMPPException with the stream error that was the root case of the
+ * exception. When a stream error is received from the server then the underlying
+ * TCP connection will be closed by the server.
+ *
+ * @param streamError the root cause of the exception.
+ */
+ public XMPPException(StreamError streamError) {
+ super();
+ this.streamError = streamError;
+ }
+
+ /**
+ * Cretaes a new XMPPException with the XMPPError that was the root case of the
+ * exception.
+ *
+ * @param error the root cause of the exception.
+ */
+ public XMPPException(XMPPError error) {
+ super();
+ this.error = error;
+ }
+
+ /**
+ * Creates a new XMPPException with a description of the exception and the
+ * Throwable that was the root cause of the exception.
+ *
+ * @param message a description of the exception.
+ * @param wrappedThrowable the root cause of the exception.
+ */
+ public XMPPException(String message, Throwable wrappedThrowable) {
+ super(message);
+ this.wrappedThrowable = wrappedThrowable;
+ }
+
+ /**
+ * Creates a new XMPPException with a description of the exception, an XMPPError,
+ * and the Throwable that was the root cause of the exception.
+ *
+ * @param message a description of the exception.
+ * @param error the root cause of the exception.
+ * @param wrappedThrowable the root cause of the exception.
+ */
+ public XMPPException(String message, XMPPError error, Throwable wrappedThrowable) {
+ super(message);
+ this.error = error;
+ this.wrappedThrowable = wrappedThrowable;
+ }
+
+ /**
+ * Creates a new XMPPException with a description of the exception and the
+ * XMPPException that was the root cause of the exception.
+ *
+ * @param message a description of the exception.
+ * @param error the root cause of the exception.
+ */
+ public XMPPException(String message, XMPPError error) {
+ super(message);
+ this.error = error;
+ }
+
+ /**
+ * Returns the XMPPError asscociated with this exception, or <tt>null</tt> if there
+ * isn't one.
+ *
+ * @return the XMPPError asscociated with this exception.
+ */
+ public XMPPError getXMPPError() {
+ return error;
+ }
+
+ /**
+ * Returns the StreamError asscociated with this exception, or <tt>null</tt> if there
+ * isn't one. The underlying TCP connection is closed by the server after sending the
+ * stream error to the client.
+ *
+ * @return the StreamError asscociated with this exception.
+ */
+ public StreamError getStreamError() {
+ return streamError;
+ }
+
+ /**
+ * Returns the Throwable asscociated with this exception, or <tt>null</tt> if there
+ * isn't one.
+ *
+ * @return the Throwable asscociated with this exception.
+ */
+ public Throwable getWrappedThrowable() {
+ return wrappedThrowable;
+ }
+
+ public void printStackTrace() {
+ printStackTrace(System.err);
+ }
+
+ public void printStackTrace(PrintStream out) {
+ super.printStackTrace(out);
+ if (wrappedThrowable != null) {
+ out.println("Nested Exception: ");
+ wrappedThrowable.printStackTrace(out);
+ }
+ }
+
+ public void printStackTrace(PrintWriter out) {
+ super.printStackTrace(out);
+ if (wrappedThrowable != null) {
+ out.println("Nested Exception: ");
+ wrappedThrowable.printStackTrace(out);
+ }
+ }
+
+ public String getMessage() {
+ String msg = super.getMessage();
+ // If the message was not set, but there is an XMPPError, return the
+ // XMPPError as the message.
+ if (msg == null && error != null) {
+ return error.toString();
+ }
+ else if (msg == null && streamError != null) {
+ return streamError.toString();
+ }
+ return msg;
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ String message = super.getMessage();
+ if (message != null) {
+ buf.append(message).append(": ");
+ }
+ if (error != null) {
+ buf.append(error);
+ }
+ if (streamError != null) {
+ buf.append(streamError);
+ }
+ if (wrappedThrowable != null) {
+ buf.append("\n -- caused by: ").append(wrappedThrowable);
+ }
+
+ return buf.toString();
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java
new file mode 100644
index 000000000..a3757cb2a
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java
@@ -0,0 +1,161 @@
+package org.jivesoftware.smack.debugger;
+
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.*;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Very simple debugger that prints to the console (stdout) the sent and received stanzas. Use
+ * this debugger with caution since printing to the console is an expensive operation that may
+ * even block the thread since only one thread may print at a time.<p>
+ * <p/>
+ * It is possible to not only print the raw sent and received stanzas but also the interpreted
+ * packets by Smack. By default interpreted packets won't be printed. To enable this feature
+ * just change the <tt>printInterpreted</tt> static variable to <tt>true</tt>.
+ *
+ * @author Gaston Dombiak
+ */
+public class ConsoleDebugger implements SmackDebugger {
+
+ public static boolean printInterpreted = false;
+ private SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa");
+
+ private XMPPConnection connection = null;
+
+ private PacketListener listener = null;
+ private ConnectionListener connListener = null;
+
+ private Writer writer;
+ private Reader reader;
+ private ReaderListener readerListener;
+ private WriterListener writerListener;
+
+ public ConsoleDebugger(XMPPConnection connection, Writer writer, Reader reader) {
+ this.connection = connection;
+ this.writer = writer;
+ this.reader = reader;
+ createDebug();
+ }
+
+ /**
+ * Creates the listeners that will print in the console when new activity is detected.
+ */
+ private void createDebug() {
+ // Create a special Reader that wraps the main Reader and logs data to the GUI.
+ ObservableReader debugReader = new ObservableReader(reader);
+ readerListener = new ReaderListener() {
+ public void read(String str) {
+ System.out.println(
+ dateFormatter.format(new Date()) + " RCV (" + connection.hashCode() +
+ "): " +
+ str);
+ }
+ };
+ debugReader.addReaderListener(readerListener);
+
+ // Create a special Writer that wraps the main Writer and logs data to the GUI.
+ ObservableWriter debugWriter = new ObservableWriter(writer);
+ writerListener = new WriterListener() {
+ public void write(String str) {
+ System.out.println(
+ dateFormatter.format(new Date()) + " SENT (" + connection.hashCode() +
+ "): " +
+ str);
+ }
+ };
+ debugWriter.addWriterListener(writerListener);
+
+ // Assign the reader/writer objects to use the debug versions. The packet reader
+ // and writer will use the debug versions when they are created.
+ reader = debugReader;
+ writer = debugWriter;
+
+ // Create a thread that will listen for all incoming packets and write them to
+ // the GUI. This is what we call "interpreted" packet data, since it's the packet
+ // data as Smack sees it and not as it's coming in as raw XML.
+ listener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ if (printInterpreted) {
+ System.out.println(
+ dateFormatter.format(new Date()) + " RCV PKT (" +
+ connection.hashCode() +
+ "): " +
+ packet.toXML());
+ }
+ }
+ };
+
+ connListener = new ConnectionListener() {
+ public void connectionClosed() {
+ System.out.println(
+ dateFormatter.format(new Date()) + " Connection closed (" +
+ connection.hashCode() +
+ ")");
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ System.out.println(
+ dateFormatter.format(new Date()) +
+ " Connection closed due to an exception (" +
+ connection.hashCode() +
+ ")");
+ e.printStackTrace();
+ }
+ };
+ }
+
+ public Reader newConnectionReader(Reader newReader) {
+ ((ObservableReader)reader).removeReaderListener(readerListener);
+ ObservableReader debugReader = new ObservableReader(newReader);
+ debugReader.addReaderListener(readerListener);
+ reader = debugReader;
+ return reader;
+ }
+
+ public Writer newConnectionWriter(Writer newWriter) {
+ ((ObservableWriter)writer).removeWriterListener(writerListener);
+ ObservableWriter debugWriter = new ObservableWriter(newWriter);
+ debugWriter.addWriterListener(writerListener);
+ writer = debugWriter;
+ return writer;
+ }
+
+ public void userHasLogged(String user) {
+ boolean isAnonymous = "".equals(StringUtils.parseName(user));
+ String title =
+ "User logged (" + connection.hashCode() + "): "
+ + (isAnonymous ? "" : StringUtils.parseBareAddress(user))
+ + "@"
+ + connection.getServiceName()
+ + ":"
+ + connection.getPort();
+ title += "/" + StringUtils.parseResource(user);
+ System.out.println(title);
+ // Add the connection listener to the connection so that the debugger can be notified
+ // whenever the connection is closed.
+ connection.addConnectionListener(connListener);
+ }
+
+ public Reader getReader() {
+ return reader;
+ }
+
+ public Writer getWriter() {
+ return writer;
+ }
+
+ public PacketListener getReaderListener() {
+ return listener;
+ }
+
+ public PacketListener getWriterListener() {
+ return null;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/LiteDebugger.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/LiteDebugger.java
new file mode 100644
index 000000000..3646121ce
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/LiteDebugger.java
@@ -0,0 +1,336 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.debugger;
+
+import java.awt.*;
+import java.awt.datatransfer.*;
+import java.awt.event.*;
+import java.io.*;
+
+import javax.swing.*;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.util.*;
+
+/**
+ * The LiteDebugger is a very simple debugger that allows to debug sent, received and
+ * interpreted messages.
+ *
+ * @author Gaston Dombiak
+ */
+public class LiteDebugger implements SmackDebugger {
+
+ private static final String NEWLINE = "\n";
+
+ private JFrame frame = null;
+ private XMPPConnection connection = null;
+
+ private PacketListener listener = null;
+
+ private Writer writer;
+ private Reader reader;
+ private ReaderListener readerListener;
+ private WriterListener writerListener;
+
+ public LiteDebugger(XMPPConnection connection, Writer writer, Reader reader) {
+ this.connection = connection;
+ this.writer = writer;
+ this.reader = reader;
+ createDebug();
+ }
+
+ /**
+ * Creates the debug process, which is a GUI window that displays XML traffic.
+ */
+ private void createDebug() {
+ frame = new JFrame("Smack Debug Window -- " + connection.getServiceName() + ":" +
+ connection.getPort());
+
+ // Add listener for window closing event
+ frame.addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent evt) {
+ rootWindowClosing(evt);
+ }
+ });
+
+ // We'll arrange the UI into four tabs. The first tab contains all data, the second
+ // client generated XML, the third server generated XML, and the fourth is packet
+ // data from the server as seen by Smack.
+ JTabbedPane tabbedPane = new JTabbedPane();
+
+ JPanel allPane = new JPanel();
+ allPane.setLayout(new GridLayout(3, 1));
+ tabbedPane.add("All", allPane);
+
+ // Create UI elements for client generated XML traffic.
+ final JTextArea sentText1 = new JTextArea();
+ final JTextArea sentText2 = new JTextArea();
+ sentText1.setEditable(false);
+ sentText2.setEditable(false);
+ sentText1.setForeground(new Color(112, 3, 3));
+ sentText2.setForeground(new Color(112, 3, 3));
+ allPane.add(new JScrollPane(sentText1));
+ tabbedPane.add("Sent", new JScrollPane(sentText2));
+
+ // Add pop-up menu.
+ JPopupMenu menu = new JPopupMenu();
+ JMenuItem menuItem1 = new JMenuItem("Copy");
+ menuItem1.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ // Get the clipboard
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ // Set the sent text as the new content of the clipboard
+ clipboard.setContents(new StringSelection(sentText1.getText()), null);
+ }
+ });
+
+ JMenuItem menuItem2 = new JMenuItem("Clear");
+ menuItem2.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ sentText1.setText("");
+ sentText2.setText("");
+ }
+ });
+
+ // Add listener to the text area so the popup menu can come up.
+ MouseListener popupListener = new PopupListener(menu);
+ sentText1.addMouseListener(popupListener);
+ sentText2.addMouseListener(popupListener);
+ menu.add(menuItem1);
+ menu.add(menuItem2);
+
+ // Create UI elements for server generated XML traffic.
+ final JTextArea receivedText1 = new JTextArea();
+ final JTextArea receivedText2 = new JTextArea();
+ receivedText1.setEditable(false);
+ receivedText2.setEditable(false);
+ receivedText1.setForeground(new Color(6, 76, 133));
+ receivedText2.setForeground(new Color(6, 76, 133));
+ allPane.add(new JScrollPane(receivedText1));
+ tabbedPane.add("Received", new JScrollPane(receivedText2));
+
+ // Add pop-up menu.
+ menu = new JPopupMenu();
+ menuItem1 = new JMenuItem("Copy");
+ menuItem1.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ // Get the clipboard
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ // Set the sent text as the new content of the clipboard
+ clipboard.setContents(new StringSelection(receivedText1.getText()), null);
+ }
+ });
+
+ menuItem2 = new JMenuItem("Clear");
+ menuItem2.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ receivedText1.setText("");
+ receivedText2.setText("");
+ }
+ });
+
+ // Add listener to the text area so the popup menu can come up.
+ popupListener = new PopupListener(menu);
+ receivedText1.addMouseListener(popupListener);
+ receivedText2.addMouseListener(popupListener);
+ menu.add(menuItem1);
+ menu.add(menuItem2);
+
+ // Create UI elements for interpreted XML traffic.
+ final JTextArea interpretedText1 = new JTextArea();
+ final JTextArea interpretedText2 = new JTextArea();
+ interpretedText1.setEditable(false);
+ interpretedText2.setEditable(false);
+ interpretedText1.setForeground(new Color(1, 94, 35));
+ interpretedText2.setForeground(new Color(1, 94, 35));
+ allPane.add(new JScrollPane(interpretedText1));
+ tabbedPane.add("Interpreted", new JScrollPane(interpretedText2));
+
+ // Add pop-up menu.
+ menu = new JPopupMenu();
+ menuItem1 = new JMenuItem("Copy");
+ menuItem1.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ // Get the clipboard
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ // Set the sent text as the new content of the clipboard
+ clipboard.setContents(new StringSelection(interpretedText1.getText()), null);
+ }
+ });
+
+ menuItem2 = new JMenuItem("Clear");
+ menuItem2.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ interpretedText1.setText("");
+ interpretedText2.setText("");
+ }
+ });
+
+ // Add listener to the text area so the popup menu can come up.
+ popupListener = new PopupListener(menu);
+ interpretedText1.addMouseListener(popupListener);
+ interpretedText2.addMouseListener(popupListener);
+ menu.add(menuItem1);
+ menu.add(menuItem2);
+
+ frame.getContentPane().add(tabbedPane);
+
+ frame.setSize(550, 400);
+ frame.setVisible(true);
+
+ // Create a special Reader that wraps the main Reader and logs data to the GUI.
+ ObservableReader debugReader = new ObservableReader(reader);
+ readerListener = new ReaderListener() {
+ public void read(String str) {
+ int index = str.lastIndexOf(">");
+ if (index != -1) {
+ receivedText1.append(str.substring(0, index + 1));
+ receivedText2.append(str.substring(0, index + 1));
+ receivedText1.append(NEWLINE);
+ receivedText2.append(NEWLINE);
+ if (str.length() > index) {
+ receivedText1.append(str.substring(index + 1));
+ receivedText2.append(str.substring(index + 1));
+ }
+ }
+ else {
+ receivedText1.append(str);
+ receivedText2.append(str);
+ }
+ }
+ };
+ debugReader.addReaderListener(readerListener);
+
+ // Create a special Writer that wraps the main Writer and logs data to the GUI.
+ ObservableWriter debugWriter = new ObservableWriter(writer);
+ writerListener = new WriterListener() {
+ public void write(String str) {
+ sentText1.append(str);
+ sentText2.append(str);
+ if (str.endsWith(">")) {
+ sentText1.append(NEWLINE);
+ sentText2.append(NEWLINE);
+ }
+ }
+ };
+ debugWriter.addWriterListener(writerListener);
+
+ // Assign the reader/writer objects to use the debug versions. The packet reader
+ // and writer will use the debug versions when they are created.
+ reader = debugReader;
+ writer = debugWriter;
+
+ // Create a thread that will listen for all incoming packets and write them to
+ // the GUI. This is what we call "interpreted" packet data, since it's the packet
+ // data as Smack sees it and not as it's coming in as raw XML.
+ listener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ interpretedText1.append(packet.toXML());
+ interpretedText2.append(packet.toXML());
+ interpretedText1.append(NEWLINE);
+ interpretedText2.append(NEWLINE);
+ }
+ };
+ }
+
+ /**
+ * Notification that the root window is closing. Stop listening for received and
+ * transmitted packets.
+ *
+ * @param evt the event that indicates that the root window is closing
+ */
+ public void rootWindowClosing(WindowEvent evt) {
+ connection.removePacketListener(listener);
+ ((ObservableReader)reader).removeReaderListener(readerListener);
+ ((ObservableWriter)writer).removeWriterListener(writerListener);
+ }
+
+ /**
+ * Listens for debug window popup dialog events.
+ */
+ private class PopupListener extends MouseAdapter {
+ JPopupMenu popup;
+
+ PopupListener(JPopupMenu popupMenu) {
+ popup = popupMenu;
+ }
+
+ public void mousePressed(MouseEvent e) {
+ maybeShowPopup(e);
+ }
+
+ public void mouseReleased(MouseEvent e) {
+ maybeShowPopup(e);
+ }
+
+ private void maybeShowPopup(MouseEvent e) {
+ if (e.isPopupTrigger()) {
+ popup.show(e.getComponent(), e.getX(), e.getY());
+ }
+ }
+ }
+
+ public Reader newConnectionReader(Reader newReader) {
+ ((ObservableReader)reader).removeReaderListener(readerListener);
+ ObservableReader debugReader = new ObservableReader(newReader);
+ debugReader.addReaderListener(readerListener);
+ reader = debugReader;
+ return reader;
+ }
+
+ public Writer newConnectionWriter(Writer newWriter) {
+ ((ObservableWriter)writer).removeWriterListener(writerListener);
+ ObservableWriter debugWriter = new ObservableWriter(newWriter);
+ debugWriter.addWriterListener(writerListener);
+ writer = debugWriter;
+ return writer;
+ }
+
+ public void userHasLogged(String user) {
+ boolean isAnonymous = "".equals(StringUtils.parseName(user));
+ String title =
+ "Smack Debug Window -- "
+ + (isAnonymous ? "" : StringUtils.parseBareAddress(user))
+ + "@"
+ + connection.getServiceName()
+ + ":"
+ + connection.getPort();
+ title += "/" + StringUtils.parseResource(user);
+ frame.setTitle(title);
+ }
+
+ public Reader getReader() {
+ return reader;
+ }
+
+ public Writer getWriter() {
+ return writer;
+ }
+
+ public PacketListener getReaderListener() {
+ return listener;
+ }
+
+ public PacketListener getWriterListener() {
+ return null;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/SmackDebugger.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/SmackDebugger.java
new file mode 100644
index 000000000..b1599ea06
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/SmackDebugger.java
@@ -0,0 +1,98 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.debugger;
+
+import java.io.*;
+
+import org.jivesoftware.smack.*;
+
+/**
+ * Interface that allows for implementing classes to debug XML traffic. That is a GUI window that
+ * displays XML traffic.<p>
+ *
+ * Every implementation of this interface <b>must</b> have a public constructor with the following
+ * arguments: XMPPConnection, Writer, Reader.
+ *
+ * @author Gaston Dombiak
+ */
+public interface SmackDebugger {
+
+ /**
+ * Called when a user has logged in to the server. The user could be an anonymous user, this
+ * means that the user would be of the form host/resource instead of the form
+ * user@host/resource.
+ *
+ * @param user the user@host/resource that has just logged in
+ */
+ public abstract void userHasLogged(String user);
+
+ /**
+ * Returns the special Reader that wraps the main Reader and logs data to the GUI.
+ *
+ * @return the special Reader that wraps the main Reader and logs data to the GUI.
+ */
+ public abstract Reader getReader();
+
+ /**
+ * Returns the special Writer that wraps the main Writer and logs data to the GUI.
+ *
+ * @return the special Writer that wraps the main Writer and logs data to the GUI.
+ */
+ public abstract Writer getWriter();
+
+ /**
+ * Returns a new special Reader that wraps the new connection Reader. The connection
+ * has been secured so the connection is using a new reader and writer. The debugger
+ * needs to wrap the new reader and writer to keep being notified of the connection
+ * traffic.
+ *
+ * @return a new special Reader that wraps the new connection Reader.
+ */
+ public abstract Reader newConnectionReader(Reader reader);
+
+ /**
+ * Returns a new special Writer that wraps the new connection Writer. The connection
+ * has been secured so the connection is using a new reader and writer. The debugger
+ * needs to wrap the new reader and writer to keep being notified of the connection
+ * traffic.
+ *
+ * @return a new special Writer that wraps the new connection Writer.
+ */
+ public abstract Writer newConnectionWriter(Writer writer);
+
+ /**
+ * Returns the thread that will listen for all incoming packets and write them to the GUI.
+ * This is what we call "interpreted" packet data, since it's the packet data as Smack sees
+ * it and not as it's coming in as raw XML.
+ *
+ * @return the PacketListener that will listen for all incoming packets and write them to
+ * the GUI
+ */
+ public abstract PacketListener getReaderListener();
+
+ /**
+ * Returns the thread that will listen for all outgoing packets and write them to the GUI.
+ *
+ * @return the PacketListener that will listen for all sent packets and write them to
+ * the GUI
+ */
+ public abstract PacketListener getWriterListener();
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/package.html
new file mode 100644
index 000000000..afb861f5e
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/package.html
@@ -0,0 +1 @@
+<body>Core debugger functionality.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/AndFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/AndFilter.java
new file mode 100644
index 000000000..2ca4a1c2b
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/AndFilter.java
@@ -0,0 +1,103 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Implements the logical AND operation over two or more packet filters.
+ * In other words, packets pass this filter if they pass <b>all</b> of the filters.
+ *
+ * @author Matt Tucker
+ */
+public class AndFilter implements PacketFilter {
+
+ /**
+ * The current number of elements in the filter.
+ */
+ private int size;
+
+ /**
+ * The list of filters.
+ */
+ private PacketFilter [] filters;
+
+ /**
+ * Creates an empty AND filter. Filters should be added using the
+ * {@link #addFilter(PacketFilter)} method.
+ */
+ public AndFilter() {
+ size = 0;
+ filters = new PacketFilter[3];
+ }
+
+ /**
+ * Creates an AND filter using the two specified filters.
+ *
+ * @param filter1 the first packet filter.
+ * @param filter2 the second packet filter.
+ */
+ public AndFilter(PacketFilter filter1, PacketFilter filter2) {
+ if (filter1 == null || filter2 == null) {
+ throw new IllegalArgumentException("Parameters cannot be null.");
+ }
+ size = 2;
+ filters = new PacketFilter[2];
+ filters[0] = filter1;
+ filters[1] = filter2;
+ }
+
+ /**
+ * Adds a filter to the filter list for the AND operation. A packet
+ * will pass the filter if all of the filters in the list accept it.
+ *
+ * @param filter a filter to add to the filter list.
+ */
+ public void addFilter(PacketFilter filter) {
+ if (filter == null) {
+ throw new IllegalArgumentException("Parameter cannot be null.");
+ }
+ // If there is no more room left in the filters array, expand it.
+ if (size == filters.length) {
+ PacketFilter [] newFilters = new PacketFilter[filters.length+2];
+ for (int i=0; i<filters.length; i++) {
+ newFilters[i] = filters[i];
+ }
+ filters = newFilters;
+ }
+ // Add the new filter to the array.
+ filters[size] = filter;
+ size++;
+ }
+
+ public boolean accept(Packet packet) {
+ for (int i=0; i<size; i++) {
+ if (!filters[i].accept(packet)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public String toString() {
+ return filters.toString();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromContainsFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromContainsFilter.java
new file mode 100644
index 000000000..7b5862184
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromContainsFilter.java
@@ -0,0 +1,54 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets where the "from" field contains a specified value.
+ *
+ * @author Matt Tucker
+ */
+public class FromContainsFilter implements PacketFilter {
+
+ private String from;
+
+ /**
+ * Creates a "from" contains filter using the "from" field part.
+ *
+ * @param from the from field value the packet must contain.
+ */
+ public FromContainsFilter(String from) {
+ if (from == null) {
+ throw new IllegalArgumentException("Parameter cannot be null.");
+ }
+ this.from = from.toLowerCase();
+ }
+
+ public boolean accept(Packet packet) {
+ if (packet.getFrom() == null) {
+ return false;
+ }
+ else {
+ return packet.getFrom().toLowerCase().indexOf(from) != -1;
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromMatchesFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromMatchesFilter.java
new file mode 100644
index 000000000..1562d5cac
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromMatchesFilter.java
@@ -0,0 +1,71 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Filter for packets where the "from" field exactly matches a specified JID. If the specified
+ * address is a bare JID then the filter will match any address whose bare JID matches the
+ * specified JID. But if the specified address is a full JID then the filter will only match
+ * if the sender of the packet matches the specified resource.
+ *
+ * @author Gaston Dombiak
+ */
+public class FromMatchesFilter implements PacketFilter {
+
+ private String address;
+ /**
+ * Flag that indicates if the checking will be done against bare JID addresses or full JIDs.
+ */
+ private boolean matchBareJID = false;
+
+ /**
+ * Creates a "from" filter using the "from" field part. If the specified address is a bare JID
+ * then the filter will match any address whose bare JID matches the specified JID. But if the
+ * specified address is a full JID then the filter will only match if the sender of the packet
+ * matches the specified resource.
+ *
+ * @param address the from field value the packet must match. Could be a full or bare JID.
+ */
+ public FromMatchesFilter(String address) {
+ if (address == null) {
+ throw new IllegalArgumentException("Parameter cannot be null.");
+ }
+ this.address = address.toLowerCase();
+ matchBareJID = "".equals(StringUtils.parseResource(address));
+ }
+
+ public boolean accept(Packet packet) {
+ if (packet.getFrom() == null) {
+ return false;
+ }
+ else if (matchBareJID) {
+ // Check if the bare JID of the sender of the packet matches the specified JID
+ return packet.getFrom().toLowerCase().startsWith(address);
+ }
+ else {
+ // Check if the full JID of the sender of the packet matches the specified JID
+ return address.equals(packet.getFrom().toLowerCase());
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/IQTypeFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/IQTypeFilter.java
new file mode 100644
index 000000000..efe600307
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/IQTypeFilter.java
@@ -0,0 +1,48 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * A filter for IQ packet types. Returns true only if the packet is an IQ packet
+ * and it matches the type provided in the constructor.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class IQTypeFilter implements PacketFilter {
+
+ private IQ.Type type;
+
+ public IQTypeFilter(IQ.Type type) {
+ this.type = type;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.filter.PacketFilter#accept(org.jivesoftware.smack.packet.Packet)
+ */
+ public boolean accept(Packet packet) {
+ return (packet instanceof IQ && ((IQ) packet).getType().equals(type));
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/MessageTypeFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/MessageTypeFilter.java
new file mode 100644
index 000000000..618ca6793
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/MessageTypeFilter.java
@@ -0,0 +1,54 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets of a specific type of Message (e.g. CHAT).
+ *
+ * @see org.jivesoftware.smack.packet.Message.Type
+ * @author Ward Harold
+ */
+public class MessageTypeFilter implements PacketFilter {
+
+ private final Message.Type type;
+
+ /**
+ * Creates a new message type filter using the specified message type.
+ *
+ * @param type the message type.
+ */
+ public MessageTypeFilter(Message.Type type) {
+ this.type = type;
+ }
+
+ public boolean accept(Packet packet) {
+ if (!(packet instanceof Message)) {
+ return false;
+ }
+ else {
+ return ((Message) packet).getType().equals(this.type);
+ }
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/NotFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/NotFilter.java
new file mode 100644
index 000000000..4e6e5494c
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/NotFilter.java
@@ -0,0 +1,50 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Implements the logical NOT operation on a packet filter. In other words, packets
+ * pass this filter if they do not pass the supplied filter.
+ *
+ * @author Matt Tucker
+ */
+public class NotFilter implements PacketFilter {
+
+ private PacketFilter filter;
+
+ /**
+ * Creates a NOT filter using the specified filter.
+ *
+ * @param filter the filter.
+ */
+ public NotFilter(PacketFilter filter) {
+ if (filter == null) {
+ throw new IllegalArgumentException("Parameter cannot be null.");
+ }
+ this.filter = filter;
+ }
+
+ public boolean accept(Packet packet) {
+ return !filter.accept(packet);
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/OrFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/OrFilter.java
new file mode 100644
index 000000000..22c3d91af
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/OrFilter.java
@@ -0,0 +1,103 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Implements the logical OR operation over two or more packet filters. In
+ * other words, packets pass this filter if they pass <b>any</b> of the filters.
+ *
+ * @author Matt Tucker
+ */
+public class OrFilter implements PacketFilter {
+
+ /**
+ * The current number of elements in the filter.
+ */
+ private int size;
+
+ /**
+ * The list of filters.
+ */
+ private PacketFilter [] filters;
+
+ /**
+ * Creates an empty OR filter. Filters should be added using the
+ * {@link #addFilter(PacketFilter)} method.
+ */
+ public OrFilter() {
+ size = 0;
+ filters = new PacketFilter[3];
+ }
+
+ /**
+ * Creates an OR filter using the two specified filters.
+ *
+ * @param filter1 the first packet filter.
+ * @param filter2 the second packet filter.
+ */
+ public OrFilter(PacketFilter filter1, PacketFilter filter2) {
+ if (filter1 == null || filter2 == null) {
+ throw new IllegalArgumentException("Parameters cannot be null.");
+ }
+ size = 2;
+ filters = new PacketFilter[2];
+ filters[0] = filter1;
+ filters[1] = filter2;
+ }
+
+ /**
+ * Adds a filter to the filter list for the OR operation. A packet
+ * will pass the filter if any filter in the list accepts it.
+ *
+ * @param filter a filter to add to the filter list.
+ */
+ public void addFilter(PacketFilter filter) {
+ if (filter == null) {
+ throw new IllegalArgumentException("Parameter cannot be null.");
+ }
+ // If there is no more room left in the filters array, expand it.
+ if (size == filters.length) {
+ PacketFilter [] newFilters = new PacketFilter[filters.length+2];
+ for (int i=0; i<filters.length; i++) {
+ newFilters[i] = filters[i];
+ }
+ filters = newFilters;
+ }
+ // Add the new filter to the array.
+ filters[size] = filter;
+ size++;
+ }
+
+ public boolean accept(Packet packet) {
+ for (int i=0; i<size; i++) {
+ if (filters[i].accept(packet)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public String toString() {
+ return filters.toString();
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java
new file mode 100644
index 000000000..b46c118cc
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java
@@ -0,0 +1,51 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets with a particular type of packet extension.
+ *
+ * @author Matt Tucker
+ */
+public class PacketExtensionFilter implements PacketFilter {
+
+ private String elementName;
+ private String namespace;
+
+ /**
+ * Creates a new packet extension filter. Packets will pass the filter if
+ * they have a packet extension that matches the specified element name
+ * and namespace.
+ *
+ * @param elementName the XML element name of the packet extension.
+ * @param namespace the XML namespace of the packet extension.
+ */
+ public PacketExtensionFilter(String elementName, String namespace) {
+ this.elementName = elementName;
+ this.namespace = namespace;
+ }
+
+ public boolean accept(Packet packet) {
+ return packet.getExtension(elementName, namespace) != null;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketFilter.java
new file mode 100644
index 000000000..fbdcac5a1
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketFilter.java
@@ -0,0 +1,63 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Defines a way to filter packets for particular attributes. Packet filters are
+ * used when constructing packet listeners or collectors -- the filter defines
+ * what packets match the criteria of the collector or listener for further
+ * packet processing.<p>
+ *
+ * Several pre-defined filters are defined. These filters can be logically combined
+ * for more complex packet filtering by using the
+ * {@link org.jivesoftware.smack.filter.AndFilter AndFilter} and
+ * {@link org.jivesoftware.smack.filter.OrFilter OrFilter} filters. It's also possible
+ * to define your own filters by implementing this interface. The code example below
+ * creates a trivial filter for packets with a specific ID.
+ *
+ * <pre>
+ * // Use an anonymous inner class to define a packet filter that returns
+ * // all packets that have a packet ID of "RS145".
+ * PacketFilter myFilter = new PacketFilter() {
+ * public boolean accept(Packet packet) {
+ * return "RS145".equals(packet.getPacketID());
+ * }
+ * };
+ * // Create a new packet collector using the filter we created.
+ * PacketCollector myCollector = packetReader.createPacketCollector(myFilter);
+ * </pre>
+ *
+ * @see org.jivesoftware.smack.PacketCollector
+ * @see org.jivesoftware.smack.PacketListener
+ * @author Matt Tucker
+ */
+public interface PacketFilter {
+
+ /**
+ * Tests whether or not the specified packet should pass the filter.
+ *
+ * @param packet the packet to test.
+ * @return true if and only if <tt>packet</tt> passes the filter.
+ */
+ public boolean accept(Packet packet);
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketIDFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketIDFilter.java
new file mode 100644
index 000000000..03a007f37
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketIDFilter.java
@@ -0,0 +1,49 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets with a particular packet ID.
+ *
+ * @author Matt Tucker
+ */
+public class PacketIDFilter implements PacketFilter {
+
+ private String packetID;
+
+ /**
+ * Creates a new packet ID filter using the specified packet ID.
+ *
+ * @param packetID the packet ID to filter for.
+ */
+ public PacketIDFilter(String packetID) {
+ if (packetID == null) {
+ throw new IllegalArgumentException("Packet ID cannot be null.");
+ }
+ this.packetID = packetID;
+ }
+
+ public boolean accept(Packet packet) {
+ return packetID.equals(packet.getPacketID());
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketTypeFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketTypeFilter.java
new file mode 100644
index 000000000..1a736e114
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketTypeFilter.java
@@ -0,0 +1,58 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets of a particular type. The type is given as a Class object, so
+ * example types would:
+ * <ul>
+ * <li><tt>Message.class</tt>
+ * <li><tt>IQ.class</tt>
+ * <li><tt>Presence.class</tt>
+ * </ul>
+ *
+ * @author Matt Tucker
+ */
+public class PacketTypeFilter implements PacketFilter {
+
+ Class packetType;
+
+ /**
+ * Creates a new packet type filter that will filter for packets that are the
+ * same type as <tt>packetType</tt>.
+ *
+ * @param packetType the Class type.
+ */
+ public PacketTypeFilter(Class packetType) {
+ // Ensure the packet type is a sub-class of Packet.
+ if (!Packet.class.isAssignableFrom(packetType)) {
+ throw new IllegalArgumentException("Packet type must be a sub-class of Packet.");
+ }
+ this.packetType = packetType;
+ }
+
+ public boolean accept(Packet packet) {
+ return packetType.isInstance(packet);
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ThreadFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ThreadFilter.java
new file mode 100644
index 000000000..b9c296d11
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ThreadFilter.java
@@ -0,0 +1,55 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Message;
+
+/**
+ * Filters for message packets with a particular thread value.
+ *
+ * @author Matt Tucker
+ */
+public class ThreadFilter implements PacketFilter {
+
+ private String thread;
+
+ /**
+ * Creates a new thread filter using the specified thread value.
+ *
+ * @param thread the thread value to filter for.
+ */
+ public ThreadFilter(String thread) {
+ if (thread == null) {
+ throw new IllegalArgumentException("Thread cannot be null.");
+ }
+ this.thread = thread;
+ }
+
+ public boolean accept(Packet packet) {
+ if (packet instanceof Message) {
+ return thread.equals(((Message)packet).getThread());
+ }
+ else {
+ return false;
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ToContainsFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ToContainsFilter.java
new file mode 100644
index 000000000..1ec8a8a05
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ToContainsFilter.java
@@ -0,0 +1,55 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets where the "to" field contains a specified value. For example,
+ * the filter could be used to listen for all packets sent to a group chat nickname.
+ *
+ * @author Matt Tucker
+ */
+public class ToContainsFilter implements PacketFilter {
+
+ private String to;
+
+ /**
+ * Creates a "to" contains filter using the "to" field part.
+ *
+ * @param to the to field value the packet must contain.
+ */
+ public ToContainsFilter(String to) {
+ if (to == null) {
+ throw new IllegalArgumentException("Parameter cannot be null.");
+ }
+ this.to = to.toLowerCase();
+ }
+
+ public boolean accept(Packet packet) {
+ if (packet.getTo() == null) {
+ return false;
+ }
+ else {
+ return packet.getTo().toLowerCase().indexOf(to) != -1;
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/package.html
new file mode 100644
index 000000000..8b3fe8034
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/package.html
@@ -0,0 +1 @@
+<body>Allows {@link org.jivesoftware.smack.PacketCollector} and {@link org.jivesoftware.smack.PacketListener} instances to filter for packets with particular attributes.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/package.html
new file mode 100644
index 000000000..2758d781a
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/package.html
@@ -0,0 +1 @@
+<body>Core classes of the Smack API.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Authentication.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Authentication.java
new file mode 100644
index 000000000..dce690b40
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Authentication.java
@@ -0,0 +1,186 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Authentication packet, which can be used to login to a XMPP server as well
+ * as discover login information from the server.
+ */
+public class Authentication extends IQ {
+
+ private String username = null;
+ private String password = null;
+ private String digest = null;
+ private String resource = null;
+
+ /**
+ * Create a new authentication packet. By default, the packet will be in
+ * "set" mode in order to perform an actual authentication with the server.
+ * In order to send a "get" request to get the available authentication
+ * modes back from the server, change the type of the IQ packet to "get":
+ * <p/>
+ * <p><tt>setType(IQ.Type.GET);</tt>
+ */
+ public Authentication() {
+ setType(IQ.Type.SET);
+ }
+
+ /**
+ * Returns the username, or <tt>null</tt> if the username hasn't been sent.
+ *
+ * @return the username.
+ */
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Sets the username.
+ *
+ * @param username the username.
+ */
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ /**
+ * Returns the plain text password or <tt>null</tt> if the password hasn't
+ * been set.
+ *
+ * @return the password.
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * Sets the plain text password.
+ *
+ * @param password the password.
+ */
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ /**
+ * Returns the password digest or <tt>null</tt> if the digest hasn't
+ * been set. Password digests offer a more secure alternative for
+ * authentication compared to plain text. The digest is the hex-encoded
+ * SHA-1 hash of the connection ID plus the user's password. If the
+ * digest and password are set, digest authentication will be used. If
+ * only one value is set, the respective authentication mode will be used.
+ *
+ * @return the digest of the user's password.
+ */
+ public String getDigest() {
+ return digest;
+ }
+
+ /**
+ * Sets the digest value using a connection ID and password. Password
+ * digests offer a more secure alternative for authentication compared to
+ * plain text. The digest is the hex-encoded SHA-1 hash of the connection ID
+ * plus the user's password. If the digest and password are set, digest
+ * authentication will be used. If only one value is set, the respective
+ * authentication mode will be used.
+ *
+ * @param connectionID the connection ID.
+ * @param password the password.
+ * @see org.jivesoftware.smack.XMPPConnection#getConnectionID()
+ */
+ public void setDigest(String connectionID, String password) {
+ this.digest = StringUtils.hash(connectionID + password);
+ }
+
+ /**
+ * Sets the digest value directly. Password digests offer a more secure
+ * alternative for authentication compared to plain text. The digest is
+ * the hex-encoded SHA-1 hash of the connection ID plus the user's password.
+ * If the digest and password are set, digest authentication will be used.
+ * If only one value is set, the respective authentication mode will be used.
+ *
+ * @param digest the digest, which is the SHA-1 hash of the connection ID
+ * the user's password, encoded as hex.
+ * @see org.jivesoftware.smack.XMPPConnection#getConnectionID()
+ */
+ public void setDigest(String digest) {
+ this.digest = digest;
+ }
+
+ /**
+ * Returns the resource or <tt>null</tt> if the resource hasn't been set.
+ *
+ * @return the resource.
+ */
+ public String getResource() {
+ return resource;
+ }
+
+ /**
+ * Sets the resource.
+ *
+ * @param resource the resource.
+ */
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:auth\">");
+ if (username != null) {
+ if (username.equals("")) {
+ buf.append("<username/>");
+ }
+ else {
+ buf.append("<username>").append(username).append("</username>");
+ }
+ }
+ if (digest != null) {
+ if (digest.equals("")) {
+ buf.append("<digest/>");
+ }
+ else {
+ buf.append("<digest>").append(digest).append("</digest>");
+ }
+ }
+ if (password != null && digest == null) {
+ if (password.equals("")) {
+ buf.append("<password/>");
+ }
+ else {
+ buf.append("<password>").append(StringUtils.escapeForXML(password)).append("</password>");
+ }
+ }
+ if (resource != null) {
+ if (resource.equals("")) {
+ buf.append("<resource/>");
+ }
+ else {
+ buf.append("<resource>").append(resource).append("</resource>");
+ }
+ }
+ buf.append("</query>");
+ return buf.toString();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Bind.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Bind.java
new file mode 100644
index 000000000..5646ecbca
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Bind.java
@@ -0,0 +1,71 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+/**
+ * IQ packet used by Smack to bind a resource and to obtain the jid assigned by the server.
+ * There are two ways to bind a resource. One is simply sending an empty Bind packet where the
+ * server will assign a new resource for this connection. The other option is to set a desired
+ * resource but the server may return a modified version of the sent resource.<p>
+ *
+ * For more information refer to the following
+ * <a href=http://www.xmpp.org/specs/rfc3920.html#bind>link</a>.
+ *
+ * @author Gaston Dombiak
+ */
+public class Bind extends IQ {
+
+ private String resource = null;
+ private String jid = null;
+
+ public Bind() {
+ setType(IQ.Type.SET);
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getJid() {
+ return jid;
+ }
+
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">");
+ if (resource != null) {
+ buf.append("<resource>").append(resource).append("</resource>");
+ }
+ if (jid != null) {
+ buf.append("<jid>").append(jid).append("</jid>");
+ }
+ buf.append("</bind>");
+ return buf.toString();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java
new file mode 100644
index 000000000..cbf1b5efe
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java
@@ -0,0 +1,134 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+import java.util.*;
+
+/**
+ * Default implementation of the PacketExtension interface. Unless a PacketExtensionProvider
+ * is registered with {@link org.jivesoftware.smack.provider.ProviderManager ProviderManager},
+ * instances of this class will be returned when getting packet extensions.<p>
+ *
+ * This class provides a very simple representation of an XML sub-document. Each element
+ * is a key in a Map with its CDATA being the value. For example, given the following
+ * XML sub-document:
+ *
+ * <pre>
+ * &lt;foo xmlns="http://bar.com"&gt;
+ * &lt;color&gt;blue&lt;/color&gt;
+ * &lt;food&gt;pizza&lt;/food&gt;
+ * &lt;/foo&gt;</pre>
+ *
+ * In this case, getValue("color") would return "blue", and getValue("food") would
+ * return "pizza". This parsing mechanism mechanism is very simplistic and will not work
+ * as desired in all cases (for example, if some of the elements have attributes. In those
+ * cases, a custom PacketExtensionProvider should be used.
+ *
+ * @author Matt Tucker
+ */
+public class DefaultPacketExtension implements PacketExtension {
+
+ private String elementName;
+ private String namespace;
+ private Map map;
+
+ /**
+ * Creates a new generic packet extension.
+ *
+ * @param elementName the name of the element of the XML sub-document.
+ * @param namespace the namespace of the element.
+ */
+ public DefaultPacketExtension(String elementName, String namespace) {
+ this.elementName = elementName;
+ this.namespace = namespace;
+ }
+
+ /**
+ * Returns the XML element name of the extension sub-packet root element.
+ *
+ * @return the XML element name of the packet extension.
+ */
+ public String getElementName() {
+ return elementName;
+ }
+
+ /**
+ * Returns the XML namespace of the extension sub-packet root element.
+ *
+ * @return the XML namespace of the packet extension.
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\">");
+ for (Iterator i=getNames(); i.hasNext(); ) {
+ String name = (String)i.next();
+ String value = getValue(name);
+ buf.append("<").append(name).append(">");
+ buf.append(value);
+ buf.append("</").append(name).append(">");
+ }
+ buf.append("</").append(elementName).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * Returns an Iterator for the names that can be used to get
+ * values of the packet extension.
+ *
+ * @return an Iterator for the names.
+ */
+ public synchronized Iterator getNames() {
+ if (map == null) {
+ return Collections.EMPTY_LIST.iterator();
+ }
+ return Collections.unmodifiableMap(new HashMap(map)).keySet().iterator();
+ }
+
+ /**
+ * Returns a packet extension value given a name.
+ *
+ * @param name the name.
+ * @return the value.
+ */
+ public synchronized String getValue(String name) {
+ if (map == null) {
+ return null;
+ }
+ return (String)map.get(name);
+ }
+
+ /**
+ * Sets a packet extension value using the given name.
+ *
+ * @param name the name.
+ * @param value the value.
+ */
+ public synchronized void setValue(String name, String value) {
+ if (map == null) {
+ map = new HashMap();
+ }
+ map.put(name, value);
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/IQ.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/IQ.java
new file mode 100644
index 000000000..926e4e422
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/IQ.java
@@ -0,0 +1,167 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * The base IQ (Info/Query) packet. IQ packets are used to get and set information
+ * on the server, including authentication, roster operations, and creating
+ * accounts. Each IQ packet has a specific type that indicates what type of action
+ * is being taken: "get", "set", "result", or "error".<p>
+ *
+ * IQ packets can contain a single child element that exists in a specific XML
+ * namespace. The combination of the element name and namespace determines what
+ * type of IQ packet it is. Some example IQ subpacket snippets:<ul>
+ *
+ * <li>&lt;query xmlns="jabber:iq:auth"&gt; -- an authentication IQ.
+ * <li>&lt;query xmlns="jabber:iq:private"&gt; -- a private storage IQ.
+ * <li>&lt;pubsub xmlns="http://jabber.org/protocol/pubsub"&gt; -- a pubsub IQ.
+ * </ul>
+ *
+ * @author Matt Tucker
+ */
+public abstract class IQ extends Packet {
+
+ private Type type = Type.GET;
+
+ /**
+ * Returns the type of the IQ packet.
+ *
+ * @return the type of the IQ packet.
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Sets the type of the IQ packet.
+ *
+ * @param type the type of the IQ packet.
+ */
+ public void setType(Type type) {
+ if (type == null) {
+ this.type = Type.GET;
+ }
+ else {
+ this.type = type;
+ }
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<iq ");
+ if (getPacketID() != null) {
+ buf.append("id=\"" + getPacketID() + "\" ");
+ }
+ if (getTo() != null) {
+ buf.append("to=\"").append(StringUtils.escapeForXML(getTo())).append("\" ");
+ }
+ if (getFrom() != null) {
+ buf.append("from=\"").append(StringUtils.escapeForXML(getFrom())).append("\" ");
+ }
+ if (type == null) {
+ buf.append("type=\"get\">");
+ }
+ else {
+ buf.append("type=\"").append(getType()).append("\">");
+ }
+ // Add the query section if there is one.
+ String queryXML = getChildElementXML();
+ if (queryXML != null) {
+ buf.append(queryXML);
+ }
+ // Add the error sub-packet, if there is one.
+ XMPPError error = getError();
+ if (error != null) {
+ buf.append(error.toXML());
+ }
+ buf.append("</iq>");
+ return buf.toString();
+ }
+
+ /**
+ * Returns the sub-element XML section of the IQ packet, or <tt>null</tt> if there
+ * isn't one. Packet extensions <b>must</b> be included, if any are defined.<p>
+ *
+ * Extensions of this class must override this method.
+ *
+ * @return the child element section of the IQ XML.
+ */
+ public abstract String getChildElementXML();
+
+ /**
+ * A class to represent the type of the IQ packet. The types are:
+ *
+ * <ul>
+ * <li>IQ.Type.GET
+ * <li>IQ.Type.SET
+ * <li>IQ.Type.RESULT
+ * <li>IQ.Type.ERROR
+ * </ul>
+ */
+ public static class Type {
+
+ public static final Type GET = new Type("get");
+ public static final Type SET = new Type("set");
+ public static final Type RESULT = new Type("result");
+ public static final Type ERROR = new Type("error");
+
+ /**
+ * Converts a String into the corresponding types. Valid String values
+ * that can be converted to types are: "get", "set", "result", and "error".
+ *
+ * @param type the String value to covert.
+ * @return the corresponding Type.
+ */
+ public static Type fromString(String type) {
+ if (type == null) {
+ return null;
+ }
+ type = type.toLowerCase();
+ if (GET.toString().equals(type)) {
+ return GET;
+ }
+ else if (SET.toString().equals(type)) {
+ return SET;
+ }
+ else if (ERROR.toString().equals(type)) {
+ return ERROR;
+ }
+ else if (RESULT.toString().equals(type)) {
+ return RESULT;
+ }
+ else {
+ return null;
+ }
+ }
+
+ private String value;
+
+ private Type(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Message.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Message.java
new file mode 100644
index 000000000..193b24d1f
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Message.java
@@ -0,0 +1,273 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Represents XMPP message packets. A message can be one of several types:
+ *
+ * <ul>
+ * <li>Message.Type.NORMAL -- (Default) a normal text message used in email like interface.
+ * <li>Message.Type.CHAT -- a typically short text message used in line-by-line chat interfaces.
+ * <li>Message.Type.GROUP_CHAT -- a chat message sent to a groupchat server for group chats.
+ * <li>Message.Type.HEADLINE -- a text message to be displayed in scrolling marquee displays.
+ * <li>Message.Type.ERROR -- indicates a messaging error.
+ * </ul>
+ *
+ * For each message type, different message fields are typically used as follows:
+ * <p>
+ * <table border="1">
+ * <tr><td>&nbsp;</td><td colspan="5"><b>Message type</b></td></tr>
+ * <tr><td><i>Field</i></td><td><b>Normal</b></td><td><b>Chat</b></td><td><b>Group Chat</b></td><td><b>Headline</b></td><td><b>XMPPError</b></td></tr>
+ * <tr><td><i>subject</i></td> <td>SHOULD</td><td>SHOULD NOT</td><td>SHOULD NOT</td><td>SHOULD NOT</td><td>SHOULD NOT</td></tr>
+ * <tr><td><i>thread</i></td> <td>OPTIONAL</td><td>SHOULD</td><td>OPTIONAL</td><td>OPTIONAL</td><td>SHOULD NOT</td></tr>
+ * <tr><td><i>body</i></td> <td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD NOT</td></tr>
+ * <tr><td><i>error</i></td> <td>MUST NOT</td><td>MUST NOT</td><td>MUST NOT</td><td>MUST NOT</td><td>MUST</td></tr>
+ * </table>
+ *
+ * @author Matt Tucker
+ */
+public class Message extends Packet {
+
+ private Type type = Type.NORMAL;
+ private String subject = null;
+ private String body = null;
+ private String thread = null;
+
+ /**
+ * Creates a new, "normal" message.
+ */
+ public Message() {
+ }
+
+ /**
+ * Creates a new "normal" message to the specified recipient.
+ *
+ * @param to the recipient of the message.
+ */
+ public Message(String to) {
+ if (to == null) {
+ throw new IllegalArgumentException("Parameter cannot be null");
+ }
+ setTo(to);
+ }
+
+ /**
+ * Creates a new message of the specified type to a recipient.
+ *
+ * @param to the user to send the message to.
+ * @param type the message type.
+ */
+ public Message(String to, Type type) {
+ if (to == null || type == null) {
+ throw new IllegalArgumentException("Parameters cannot be null.");
+ }
+ setTo(to);
+ this.type = type;
+ }
+
+ /**
+ * Returns the type of the message.
+ *
+ * @return the type of the message.
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Sets the type of the message.
+ *
+ * @param type the type of the message.
+ */
+ public void setType(Type type) {
+ if (type == null) {
+ throw new IllegalArgumentException("Type cannot be null.");
+ }
+ this.type = type;
+ }
+
+ /**
+ * Returns the subject of the message, or null if the subject has not been set.
+ * The subject is a short description of message contents.
+ *
+ * @return the subject of the message.
+ */
+ public String getSubject() {
+ return subject;
+ }
+
+ /**
+ * Sets the subject of the message. The subject is a short description of
+ * message contents.
+ *
+ * @param subject the subject of the message.
+ */
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ /**
+ * Returns the body of the message, or null if the body has not been set. The body
+ * is the main message contents.
+ *
+ * @return the body of the message.
+ */
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * Sets the body of the message. The body is the main message contents.
+ * @param body
+ */
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ /**
+ * Returns the thread id of the message, which is a unique identifier for a sequence
+ * of "chat" messages. If no thread id is set, <tt>null</tt> will be returned.
+ *
+ * @return the thread id of the message, or <tt>null</tt> if it doesn't exist.
+ */
+ public String getThread() {
+ return thread;
+ }
+
+ /**
+ * Sets the thread id of the message, which is a unique identifier for a sequence
+ * of "chat" messages.
+ *
+ * @param thread the thread id of the message.
+ */
+ public void setThread(String thread) {
+ this.thread = thread;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<message");
+ if (getPacketID() != null) {
+ buf.append(" id=\"").append(getPacketID()).append("\"");
+ }
+ if (getTo() != null) {
+ buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\"");
+ }
+ if (getFrom() != null) {
+ buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\"");
+ }
+ if (type != Type.NORMAL) {
+ buf.append(" type=\"").append(type).append("\"");
+ }
+ buf.append(">");
+ if (subject != null) {
+ buf.append("<subject>").append(StringUtils.escapeForXML(subject)).append("</subject>");
+ }
+ if (body != null) {
+ buf.append("<body>").append(StringUtils.escapeForXML(body)).append("</body>");
+ }
+ if (thread != null) {
+ buf.append("<thread>").append(thread).append("</thread>");
+ }
+ // Append the error subpacket if the message type is an error.
+ if (type == Type.ERROR) {
+ XMPPError error = getError();
+ if (error != null) {
+ buf.append(error.toXML());
+ }
+ }
+ // Add packet extensions, if any are defined.
+ buf.append(getExtensionsXML());
+ buf.append("</message>");
+ return buf.toString();
+ }
+
+ /**
+ * Represents the type of a message.
+ */
+ public static class Type {
+
+ /**
+ * (Default) a normal text message used in email like interface.
+ */
+ public static final Type NORMAL = new Type("normal");
+
+ /**
+ * Typically short text message used in line-by-line chat interfaces.
+ */
+ public static final Type CHAT = new Type("chat");
+
+ /**
+ * Chat message sent to a groupchat server for group chats.
+ */
+ public static final Type GROUP_CHAT = new Type("groupchat");
+
+ /**
+ * Text message to be displayed in scrolling marquee displays.
+ */
+ public static final Type HEADLINE = new Type("headline");
+
+ /**
+ * indicates a messaging error.
+ */
+ public static final Type ERROR = new Type("error");
+
+ /**
+ * Converts a String value into its Type representation.
+ *
+ * @param type the String value.
+ * @return the Type corresponding to the String.
+ */
+ public static Type fromString(String type) {
+ if (type == null) {
+ return NORMAL;
+ }
+ type = type.toLowerCase();
+ if (CHAT.toString().equals(type)) {
+ return CHAT;
+ }
+ else if (GROUP_CHAT.toString().equals(type)) {
+ return GROUP_CHAT;
+ }
+ else if (HEADLINE.toString().equals(type)) {
+ return HEADLINE;
+ }
+ else if (ERROR.toString().equals(type)) {
+ return ERROR;
+ }
+ else {
+ return NORMAL;
+ }
+ }
+
+ private String value;
+
+ private Type(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Packet.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Packet.java
new file mode 100644
index 000000000..a0de7a602
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Packet.java
@@ -0,0 +1,423 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.*;
+import java.io.*;
+
+/**
+ * Base class for XMPP packets. Every packet has a unique ID (which is automatically
+ * generated, but can be overriden). Optionally, the "to" and "from" fields can be set,
+ * as well as an arbitrary number of properties.
+ *
+ * Properties provide an easy mechanism for clients to share data. Each property has a
+ * String name, and a value that is a Java primitive (int, long, float, double, boolean)
+ * or any Serializable object (a Java object is Serializable when it implements the
+ * Serializable interface).
+ *
+ * @author Matt Tucker
+ */
+public abstract class Packet {
+
+ /**
+ * Constant used as packetID to indicate that a packet has no id. To indicate that a packet
+ * has no id set this constant as the packet's id. When the packet is asked for its id the
+ * answer will be <tt>null</tt>.
+ */
+ public static final String ID_NOT_AVAILABLE = "ID_NOT_AVAILABLE";
+
+ /**
+ * A prefix helps to make sure that ID's are unique across mutliple instances.
+ */
+ private static String prefix = StringUtils.randomString(5) + "-";
+
+ /**
+ * Keeps track of the current increment, which is appended to the prefix to
+ * forum a unique ID.
+ */
+ private static long id = 0;
+
+ /**
+ * Returns the next unique id. Each id made up of a short alphanumeric
+ * prefix along with a unique numeric value.
+ *
+ * @return the next id.
+ */
+ private static synchronized String nextID() {
+ return prefix + Long.toString(id++);
+ }
+
+ private String packetID = null;
+ private String to = null;
+ private String from = null;
+ private List packetExtensions = null;
+ private Map properties = null;
+ private XMPPError error = null;
+
+ /**
+ * Returns the unique ID of the packet. The returned value could be <tt>null</tt> when
+ * ID_NOT_AVAILABLE was set as the packet's id.
+ *
+ * @return the packet's unique ID or <tt>null</tt> if the packet's id is not available.
+ */
+ public String getPacketID() {
+ if (ID_NOT_AVAILABLE.equals(packetID)) {
+ return null;
+ }
+
+ if (packetID == null) {
+ packetID = nextID();
+ }
+ return packetID;
+ }
+
+ /**
+ * Sets the unique ID of the packet. To indicate that a packet has no id
+ * pass the constant ID_NOT_AVAILABLE as the packet's id value.
+ *
+ * @param packetID the unique ID for the packet.
+ */
+ public void setPacketID(String packetID) {
+ this.packetID = packetID;
+ }
+
+ /**
+ * Returns who the packet is being sent "to", or <tt>null</tt> if
+ * the value is not set. The XMPP protocol often makes the "to"
+ * attribute optional, so it does not always need to be set.
+ *
+ * @return who the packet is being sent to, or <tt>null</tt> if the
+ * value has not been set.
+ */
+ public String getTo() {
+ return to;
+ }
+
+ /**
+ * Sets who the packet is being sent "to". The XMPP protocol often makes
+ * the "to" attribute optional, so it does not always need to be set.
+ *
+ * @param to who the packet is being sent to.
+ */
+ public void setTo(String to) {
+ this.to = to;
+ }
+
+ /**
+ * Returns who the packet is being sent "from" or <tt>null</tt> if
+ * the value is not set. The XMPP protocol often makes the "from"
+ * attribute optional, so it does not always need to be set.
+ *
+ * @return who the packet is being sent from, or <tt>null</tt> if the
+ * valud has not been set.
+ */
+ public String getFrom() {
+ return from;
+ }
+
+ /**
+ * Sets who the packet is being sent "from". The XMPP protocol often
+ * makes the "from" attribute optional, so it does not always need to
+ * be set.
+ *
+ * @param from who the packet is being sent to.
+ */
+ public void setFrom(String from) {
+ this.from = from;
+ }
+
+ /**
+ * Returns the error associated with this packet, or <tt>null</tt> if there are
+ * no errors.
+ *
+ * @return the error sub-packet or <tt>null</tt> if there isn't an error.
+ */
+ public XMPPError getError() {
+ return error;
+ }
+
+ /**
+ * Sets the error for this packet.
+ *
+ * @param error the error to associate with this packet.
+ */
+ public void setError(XMPPError error) {
+ this.error = error;
+ }
+
+ /**
+ * Returns an Iterator for the packet extensions attached to the packet.
+ *
+ * @return an Iterator for the packet extensions.
+ */
+ public synchronized Iterator getExtensions() {
+ if (packetExtensions == null) {
+ return Collections.EMPTY_LIST.iterator();
+ }
+ return Collections.unmodifiableList(new ArrayList(packetExtensions)).iterator();
+ }
+
+ /**
+ * Returns the first packet extension that matches the specified element name and
+ * namespace, or <tt>null</tt> if it doesn't exist. Packet extensions are
+ * are arbitrary XML sub-documents in standard XMPP packets. By default, a
+ * DefaultPacketExtension instance will be returned for each extension. However,
+ * PacketExtensionProvider instances can be registered with the
+ * {@link org.jivesoftware.smack.provider.ProviderManager ProviderManager}
+ * class to handle custom parsing. In that case, the type of the Object
+ * will be determined by the provider.
+ *
+ * @param elementName the XML element name of the packet extension.
+ * @param namespace the XML element namespace of the packet extension.
+ * @return the extension, or <tt>null</tt> if it doesn't exist.
+ */
+ public synchronized PacketExtension getExtension(String elementName, String namespace) {
+ if (packetExtensions == null || elementName == null || namespace == null) {
+ return null;
+ }
+ for (Iterator i=packetExtensions.iterator(); i.hasNext(); ) {
+ PacketExtension ext = (PacketExtension)i.next();
+ if (elementName.equals(ext.getElementName()) && namespace.equals(ext.getNamespace())) {
+ return ext;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a packet extension to the packet.
+ *
+ * @param extension a packet extension.
+ */
+ public synchronized void addExtension(PacketExtension extension) {
+ if (packetExtensions == null) {
+ packetExtensions = new ArrayList();
+ }
+ packetExtensions.add(extension);
+ }
+
+ /**
+ * Removes a packet extension from the packet.
+ *
+ * @param extension the packet extension to remove.
+ */
+ public synchronized void removeExtension(PacketExtension extension) {
+ if (packetExtensions != null) {
+ packetExtensions.remove(extension);
+ }
+ }
+
+ /**
+ * Returns the packet property with the specified name or <tt>null</tt> if the
+ * property doesn't exist. Property values that were orginally primitives will
+ * be returned as their object equivalent. For example, an int property will be
+ * returned as an Integer, a double as a Double, etc.
+ *
+ * @param name the name of the property.
+ * @return the property, or <tt>null</tt> if the property doesn't exist.
+ */
+ public synchronized Object getProperty(String name) {
+ if (properties == null) {
+ return null;
+ }
+ return properties.get(name);
+ }
+
+ /**
+ * Sets a packet property with an int value.
+ *
+ * @param name the name of the property.
+ * @param value the value of the property.
+ */
+ public void setProperty(String name, int value) {
+ setProperty(name, new Integer(value));
+ }
+
+ /**
+ * Sets a packet property with a long value.
+ *
+ * @param name the name of the property.
+ * @param value the value of the property.
+ */
+ public void setProperty(String name, long value) {
+ setProperty(name, new Long(value));
+ }
+
+ /**
+ * Sets a packet property with a float value.
+ *
+ * @param name the name of the property.
+ * @param value the value of the property.
+ */
+ public void setProperty(String name, float value) {
+ setProperty(name, new Float(value));
+ }
+
+ /**
+ * Sets a packet property with a double value.
+ *
+ * @param name the name of the property.
+ * @param value the value of the property.
+ */
+ public void setProperty(String name, double value) {
+ setProperty(name, new Double(value));
+ }
+
+ /**
+ * Sets a packet property with a bboolean value.
+ *
+ * @param name the name of the property.
+ * @param value the value of the property.
+ */
+ public void setProperty(String name, boolean value) {
+ setProperty(name, new Boolean(value));
+ }
+
+ /**
+ * Sets a property with an Object as the value. The value must be Serializable
+ * or an IllegalArgumentException will be thrown.
+ *
+ * @param name the name of the property.
+ * @param value the value of the property.
+ */
+ public synchronized void setProperty(String name, Object value) {
+ if (!(value instanceof Serializable)) {
+ throw new IllegalArgumentException("Value must be serialiazble");
+ }
+ if (properties == null) {
+ properties = new HashMap();
+ }
+ properties.put(name, value);
+ }
+
+ /**
+ * Deletes a property.
+ *
+ * @param name the name of the property to delete.
+ */
+ public synchronized void deleteProperty(String name) {
+ if (properties == null) {
+ return;
+ }
+ properties.remove(name);
+ }
+
+ /**
+ * Returns an Iterator for all the property names that are set.
+ *
+ * @return an Iterator for all property names.
+ */
+ public synchronized Iterator getPropertyNames() {
+ if (properties == null) {
+ return Collections.EMPTY_LIST.iterator();
+ }
+ return properties.keySet().iterator();
+ }
+
+ /**
+ * Returns the packet as XML. Every concrete extension of Packet must implement
+ * this method. In addition to writing out packet-specific data, every sub-class
+ * should also write out the error and the extensions data if they are defined.
+ *
+ * @return the XML format of the packet as a String.
+ */
+ public abstract String toXML();
+
+ /**
+ * Returns the extension sub-packets (including properties data) as an XML
+ * String, or the Empty String if there are no packet extensions.
+ *
+ * @return the extension sub-packets as XML or the Empty String if there
+ * are no packet extensions.
+ */
+ protected synchronized String getExtensionsXML() {
+ StringBuffer buf = new StringBuffer();
+ // Add in all standard extension sub-packets.
+ Iterator extensions = getExtensions();
+ while (extensions.hasNext()) {
+ PacketExtension extension = (PacketExtension)extensions.next();
+ buf.append(extension.toXML());
+ }
+ // Add in packet properties.
+ if (properties != null && !properties.isEmpty()) {
+ buf.append("<properties xmlns=\"http://www.jivesoftware.com/xmlns/xmpp/properties\">");
+ // Loop through all properties and write them out.
+ for (Iterator i=getPropertyNames(); i.hasNext(); ) {
+ String name = (String)i.next();
+ Object value = getProperty(name);
+ buf.append("<property>");
+ buf.append("<name>").append(StringUtils.escapeForXML(name)).append("</name>");
+ buf.append("<value type=\"");
+ if (value instanceof Integer) {
+ buf.append("integer\">").append(value).append("</value>");
+ }
+ else if (value instanceof Long) {
+ buf.append("long\">").append(value).append("</value>");
+ }
+ else if (value instanceof Float) {
+ buf.append("float\">").append(value).append("</value>");
+ }
+ else if (value instanceof Double) {
+ buf.append("double\">").append(value).append("</value>");
+ }
+ else if (value instanceof Boolean) {
+ buf.append("boolean\">").append(value).append("</value>");
+ }
+ else if (value instanceof String) {
+ buf.append("string\">");
+ buf.append(StringUtils.escapeForXML((String)value));
+ buf.append("</value>");
+ }
+ // Otherwise, it's a generic Serializable object. Serialized objects are in
+ // a binary format, which won't work well inside of XML. Therefore, we base-64
+ // encode the binary data before adding it.
+ else {
+ ByteArrayOutputStream byteStream = null;
+ ObjectOutputStream out = null;
+ try {
+ byteStream = new ByteArrayOutputStream();
+ out = new ObjectOutputStream(byteStream);
+ out.writeObject(value);
+ buf.append("java-object\">");
+ String encodedVal = StringUtils.encodeBase64(byteStream.toByteArray());
+ buf.append(encodedVal).append("</value>");
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ finally {
+ if (out != null) {
+ try { out.close(); } catch (Exception e) { }
+ }
+ if (byteStream != null) {
+ try { byteStream.close(); } catch (Exception e) { }
+ }
+ }
+ }
+ buf.append("</property>");
+ }
+ buf.append("</properties>");
+ }
+ return buf.toString();
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/PacketExtension.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/PacketExtension.java
new file mode 100644
index 000000000..e402e9d29
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/PacketExtension.java
@@ -0,0 +1,56 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+/**
+ * Interface to represent packet extensions. A packet extension is an XML subdocument
+ * with a root element name and namespace. Packet extensions are used to provide
+ * extended functionality beyond what is in the base XMPP specification. Examples of
+ * packet extensions include message events, message properties, and extra presence data.
+ * IQ packets cannot contain packet extensions.
+ *
+ * @see DefaultPacketExtension
+ * @see org.jivesoftware.smack.provider.PacketExtensionProvider
+ * @author Matt Tucker
+ */
+public interface PacketExtension {
+
+ /**
+ * Returns the root element name.
+ *
+ * @return the element name.
+ */
+ public String getElementName();
+
+ /**
+ * Returns the root element XML namespace.
+ *
+ * @return the namespace.
+ */
+ public String getNamespace();
+
+ /**
+ * Returns the XML reppresentation of the PacketExtension.
+ *
+ * @return the packet extension as XML.
+ */
+ public String toXML();
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Presence.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Presence.java
new file mode 100644
index 000000000..52e17d0f0
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Presence.java
@@ -0,0 +1,327 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Represents XMPP presence packets. Every presence packet has a type, which is one of
+ * the following values:
+ * <ul>
+ * <li><tt>Presence.Type.AVAILABLE</tt> -- (Default) indicates the user is available to
+ * receive messages.
+ * <li><tt>Presence.Type.UNAVAILABLE</tt> -- the user is unavailable to receive messages.
+ * <li><tt>Presence.Type.SUBSCRIBE</tt> -- request subscription to recipient's presence.
+ * <li><tt>Presence.Type.SUBSCRIBED</tt> -- grant subscription to sender's presence.
+ * <li><tt>Presence.Type.UNSUBSCRIBE</tt> -- request removal of subscription to sender's
+ * presence.
+ * <li><tt>Presence.Type.UNSUBSCRIBED</tt> -- grant removal of subscription to sender's
+ * presence.
+ * <li><tt>Presence.Type.ERROR</tt> -- the presence packet contains an error message.
+ * </ul><p>
+ *
+ * A number of attributes are optional:
+ * <ul>
+ * <li>Status -- free-form text describing a user's presence (i.e., gone to lunch).
+ * <li>Priority -- non-negative numerical priority of a sender's resource. The
+ * highest resource priority is the default recipient of packets not addressed
+ * to a particular resource.
+ * <li>Mode -- one of five presence modes: available (the default), chat, away,
+ * xa (extended away, and dnd (do not disturb).
+ * </ul><p>
+ *
+ * Presence packets are used for two purposes. First, to notify the server of our
+ * the clients current presence status. Second, they are used to subscribe and
+ * unsubscribe users from the roster.
+ *
+ * @see RosterPacket
+ * @author Matt Tucker
+ */
+public class Presence extends Packet {
+
+ private Type type = Type.AVAILABLE;
+ private String status = null;
+ private int priority = -1;
+ private Mode mode = Mode.AVAILABLE;
+
+ /**
+ * Creates a new presence update. Status, priority, and mode are left un-set.
+ *
+ * @param type the type.
+ */
+ public Presence(Type type) {
+ this.type = type;
+ }
+
+ /**
+ * Creates a new presence update with a specified status, priority, and mode.
+ *
+ * @param type the type.
+ * @param status a text message describing the presence update.
+ * @param priority the priority of this presence update.
+ * @param mode the mode type for this presence update.
+ */
+ public Presence(Type type, String status, int priority, Mode mode) {
+ this.type = type;
+ this.status = status;
+ this.priority = priority;
+ this.mode = mode;
+ }
+
+ /**
+ * Returns the type of this presence packet.
+ *
+ * @return the type of the presence packet.
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Sets the type of the presence packet.
+ *
+ * @param type the type of the presence packet.
+ */
+ public void setType(Type type) {
+ this.type = type;
+ }
+
+ /**
+ * Returns the status message of the presence update, or <tt>null</tt> if there
+ * is not a status. The status is free-form text describing a user's presence
+ * (i.e., "gone to lunch").
+ *
+ * @return the status message.
+ */
+ public String getStatus() {
+ return status;
+ }
+
+ /**
+ * Sets the status message of the presence update. The status is free-form text
+ * describing a user's presence (i.e., "gone to lunch").
+ *
+ * @param status the status message.
+ */
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ /**
+ * Returns the priority of the presence, or -1 if no priority has been set.
+ *
+ * @return the priority.
+ */
+ public int getPriority() {
+ return priority;
+ }
+
+ /**
+ * Sets the priority of the presence. The valid range is -128 through 128.
+ *
+ * @param priority the priority of the presence.
+ * @throws IllegalArgumentException if the priority is outside the valid range.
+ */
+ public void setPriority(int priority) {
+ if (priority < -128 || priority > 128) {
+ throw new IllegalArgumentException("Priority value " + priority +
+ " is not valid. Valid range is -128 through 128.");
+ }
+ this.priority = priority;
+ }
+
+ /**
+ * Returns the mode of the presence update.
+ *
+ * @return the mode.
+ */
+ public Mode getMode() {
+ return mode;
+ }
+
+ /**
+ * Sets the mode of the presence update. For the standard "available" state, set
+ * the mode to <tt>null</tt>.
+ *
+ * @param mode the mode.
+ */
+ public void setMode(Mode mode) {
+ this.mode = mode;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<presence");
+ if (getPacketID() != null) {
+ buf.append(" id=\"").append(getPacketID()).append("\"");
+ }
+ if (getTo() != null) {
+ buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\"");
+ }
+ if (getFrom() != null) {
+ buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\"");
+ }
+ if (type != Type.AVAILABLE) {
+ buf.append(" type=\"").append(type).append("\"");
+ }
+ buf.append(">");
+ if (status != null) {
+ buf.append("<status>").append(status).append("</status>");
+ }
+ if (priority != -1) {
+ buf.append("<priority>").append(priority).append("</priority>");
+ }
+ if (mode != null && mode != Mode.AVAILABLE) {
+ buf.append("<show>").append(mode).append("</show>");
+ }
+
+ buf.append(this.getExtensionsXML());
+
+ // Add the error sub-packet, if there is one.
+ XMPPError error = getError();
+ if (error != null) {
+ buf.append(error.toXML());
+ }
+
+ buf.append("</presence>");
+
+ return buf.toString();
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(type);
+ if (mode != null) {
+ buf.append(": ").append(mode);
+ }
+ if (status != null) {
+ buf.append(" (").append(status).append(")");
+ }
+ return buf.toString();
+ }
+
+ /**
+ * A typsafe enum class to represent the presecence type.
+ */
+ public static class Type {
+
+ public static final Type AVAILABLE = new Type("available");
+ public static final Type UNAVAILABLE = new Type("unavailable");
+ public static final Type SUBSCRIBE = new Type("subscribe");
+ public static final Type SUBSCRIBED = new Type("subscribed");
+ public static final Type UNSUBSCRIBE = new Type("unsubscribe");
+ public static final Type UNSUBSCRIBED = new Type("unsubscribed");
+ public static final Type ERROR = new Type("error");
+
+ private String value;
+
+ private Type(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+
+ /**
+ * Returns the type constant associated with the String value.
+ */
+ public static Type fromString(String value) {
+ if (value == null) {
+ return AVAILABLE;
+ }
+ value = value.toLowerCase();
+ if ("unavailable".equals(value)) {
+ return UNAVAILABLE;
+ }
+ else if ("subscribe".equals(value)) {
+ return SUBSCRIBE;
+ }
+ else if ("subscribed".equals(value)) {
+ return SUBSCRIBED;
+ }
+ else if ("unsubscribe".equals(value)) {
+ return UNSUBSCRIBE;
+ }
+ else if ("unsubscribed".equals(value)) {
+ return UNSUBSCRIBED;
+ }
+ else if ("error".equals(value)) {
+ return ERROR;
+ }
+ // Default to available.
+ else {
+ return AVAILABLE;
+ }
+ }
+ }
+
+ /**
+ * A typsafe enum class to represent the presence mode.
+ */
+ public static class Mode {
+
+ public static final Mode AVAILABLE = new Mode("available");
+ public static final Mode CHAT = new Mode("chat");
+ public static final Mode AWAY = new Mode("away");
+ public static final Mode EXTENDED_AWAY = new Mode("xa");
+ public static final Mode DO_NOT_DISTURB = new Mode("dnd");
+ public static final Mode INVISIBLE = new Mode("invisible");
+
+ private String value;
+
+ private Mode(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+
+ /**
+ * Returns the mode constant associated with the String value.
+ */
+ public static Mode fromString(String value) {
+ if (value == null) {
+ return AVAILABLE;
+ }
+ value = value.toLowerCase();
+ if (value.equals("chat")) {
+ return CHAT;
+ }
+ else if (value.equals("away")) {
+ return AWAY;
+ }
+ else if (value.equals("xa")) {
+ return EXTENDED_AWAY;
+ }
+ else if (value.equals("dnd")) {
+ return DO_NOT_DISTURB;
+ }
+ else if (value.equals("invisible")) {
+ return INVISIBLE;
+ }
+ else {
+ return AVAILABLE;
+ }
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Registration.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Registration.java
new file mode 100644
index 000000000..07b06daa7
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Registration.java
@@ -0,0 +1,113 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+import java.util.Map;
+import java.util.Iterator;
+
+/**
+ * Represents registration packets. An empty GET query will cause the server to return information
+ * about it's registration support. SET queries can be used to create accounts or update
+ * existing account information. XMPP servers may require a number of attributes to be set
+ * when creating a new account. The standard account attributes are as follows:
+ * <ul>
+ * <li>name -- the user's name.
+ * <li>first -- the user's first name.
+ * <li>last -- the user's last name.
+ * <li>email -- the user's email address.
+ * <li>city -- the user's city.
+ * <li>state -- the user's state.
+ * <li>zip -- the user's ZIP code.
+ * <li>phone -- the user's phone number.
+ * <li>url -- the user's website.
+ * <li>date -- the date the registration took place.
+ * <li>misc -- other miscellaneous information to associate with the account.
+ * <li>text -- textual information to associate with the account.
+ * <li>remove -- empty flag to remove account.
+ * </ul>
+ *
+ * @author Matt Tucker
+ */
+public class Registration extends IQ {
+
+ private String instructions = null;
+ private Map attributes = null;
+
+ /**
+ * Returns the registration instructions, or <tt>null</tt> if no instructions
+ * have been set. If present, instructions should be displayed to the end-user
+ * that will complete the registration process.
+ *
+ * @return the registration instructions, or <tt>null</tt> if there are none.
+ */
+ public String getInstructions() {
+ return instructions;
+ }
+
+ /**
+ * Sets the registration instructions.
+ *
+ * @param instructions the registration instructions.
+ */
+ public void setInstructions(String instructions) {
+ this.instructions = instructions;
+ }
+
+ /**
+ * Returns the map of String key/value pairs of account attributes.
+ *
+ * @return the account attributes.
+ */
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ /**
+ * Sets the account attributes. The map must only contain String key/value pairs.
+ *
+ * @param attributes the account attributes.
+ */
+ public void setAttributes(Map attributes) {
+ this.attributes = attributes;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:register\">");
+ if (instructions != null) {
+ buf.append("<instructions>").append(instructions).append("</instructions>");
+ }
+ if (attributes != null && attributes.size() > 0) {
+ Iterator fieldNames = attributes.keySet().iterator();
+ while (fieldNames.hasNext()) {
+ String name = (String)fieldNames.next();
+ String value = (String)attributes.get(name);
+ buf.append("<").append(name).append(">");
+ buf.append(value);
+ buf.append("</").append(name).append(">");
+ }
+ }
+ // Add packet extensions, if any are defined.
+ buf.append(getExtensionsXML());
+ buf.append("</query>");
+ return buf.toString();
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/RosterPacket.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/RosterPacket.java
new file mode 100644
index 000000000..30048b7c3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/RosterPacket.java
@@ -0,0 +1,348 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+import java.util.*;
+
+/**
+ * Represents XMPP roster packets.
+ *
+ * @author Matt Tucker
+ */
+public class RosterPacket extends IQ {
+
+ private List rosterItems = new ArrayList();
+
+ /**
+ * Adds a roster item to the packet.
+ *
+ * @param item a roster item.
+ */
+ public void addRosterItem(Item item) {
+ synchronized (rosterItems) {
+ rosterItems.add(item);
+ }
+ }
+
+ /**
+ * Returns the number of roster items in this roster packet.
+ *
+ * @return the number of roster items.
+ */
+ public int getRosterItemCount() {
+ synchronized (rosterItems) {
+ return rosterItems.size();
+ }
+ }
+
+ /**
+ * Returns an Iterator for the roster items in the packet.
+ *
+ * @return and Iterator for the roster items in the packet.
+ */
+ public Iterator getRosterItems() {
+ synchronized (rosterItems) {
+ List entries = Collections.unmodifiableList(new ArrayList(rosterItems));
+ return entries.iterator();
+ }
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:roster\">");
+ synchronized (rosterItems) {
+ for (int i=0; i<rosterItems.size(); i++) {
+ Item entry = (Item)rosterItems.get(i);
+ buf.append(entry.toXML());
+ }
+ }
+ buf.append("</query>");
+ return buf.toString();
+ }
+
+ /**
+ * A roster item, which consists of a JID, their name, the type of subscription, and
+ * the groups the roster item belongs to.
+ */
+ public static class Item {
+
+ private String user;
+ private String name;
+ private ItemType itemType;
+ private ItemStatus itemStatus;
+ private List groupNames;
+
+ /**
+ * Creates a new roster item.
+ *
+ * @param user the user.
+ * @param name the user's name.
+ */
+ public Item(String user, String name) {
+ this.user = user.toLowerCase();
+ this.name = name;
+ itemType = null;
+ itemStatus = null;
+ groupNames = new ArrayList();
+ }
+
+ /**
+ * Returns the user.
+ *
+ * @return the user.
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * Returns the user's name.
+ *
+ * @return the user's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the user's name.
+ *
+ * @param name the user's name.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the roster item type.
+ *
+ * @return the roster item type.
+ */
+ public ItemType getItemType() {
+ return itemType;
+ }
+
+ /**
+ * Sets the roster item type.
+ *
+ * @param itemType the roster item type.
+ */
+ public void setItemType(ItemType itemType) {
+ this.itemType = itemType;
+ }
+
+ /**
+ * Returns the roster item status.
+ *
+ * @return the roster item status.
+ */
+ public ItemStatus getItemStatus() {
+ return itemStatus;
+ }
+
+ /**
+ * Sets the roster item status.
+ *
+ * @param itemStatus the roster item status.
+ */
+ public void setItemStatus(ItemStatus itemStatus) {
+ this.itemStatus = itemStatus;
+ }
+
+ /**
+ * Returns an Iterator for the group names (as Strings) that the roster item
+ * belongs to.
+ *
+ * @return an Iterator for the group names.
+ */
+ public Iterator getGroupNames() {
+ synchronized (groupNames) {
+ return Collections.unmodifiableList(groupNames).iterator();
+ }
+ }
+
+ /**
+ * Adds a group name.
+ *
+ * @param groupName the group name.
+ */
+ public void addGroupName(String groupName) {
+ synchronized (groupNames) {
+ if (!groupNames.contains(groupName)) {
+ groupNames.add(groupName);
+ }
+ }
+ }
+
+ /**
+ * Removes a group name.
+ *
+ * @param groupName the group name.
+ */
+ public void removeGroupName(String groupName) {
+ synchronized (groupNames) {
+ groupNames.remove(groupName);
+ }
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<item jid=\"").append(user).append("\"");
+ if (name != null) {
+ buf.append(" name=\"").append(name).append("\"");
+ }
+ if (itemType != null) {
+ buf.append(" subscription=\"").append(itemType).append("\"");
+ }
+ if (itemStatus != null) {
+ buf.append(" ask=\"").append(itemStatus).append("\"");
+ }
+ buf.append(">");
+ synchronized (groupNames) {
+ for (int i=0; i<groupNames.size(); i++) {
+ String groupName = (String)groupNames.get(i);
+ buf.append("<group>").append(groupName).append("</group>");
+ }
+ }
+ buf.append("</item>");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * The subscription status of a roster item. An optional element that indicates
+ * the subscription status if a change request is pending.
+ */
+ public static class ItemStatus {
+
+ /**
+ * Request to subcribe.
+ */
+ public static final ItemStatus SUBSCRIPTION_PENDING = new ItemStatus("subscribe");
+
+ /**
+ * Request to unsubscribe.
+ */
+ public static final ItemStatus UNSUBCRIPTION_PENDING = new ItemStatus("unsubscribe");
+
+ public static ItemStatus fromString(String value) {
+ if (value == null) {
+ return null;
+ }
+ value = value.toLowerCase();
+ if ("unsubscribe".equals(value)) {
+ return SUBSCRIPTION_PENDING;
+ }
+ else if ("subscribe".equals(value)) {
+ return SUBSCRIPTION_PENDING;
+ }
+ else {
+ return null;
+ }
+ }
+
+ private String value;
+
+ /**
+ * Returns the item status associated with the specified string.
+ *
+ * @param value the item status.
+ */
+ private ItemStatus(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+
+ /**
+ * The subscription type of a roster item.
+ */
+ public static class ItemType {
+
+ /**
+ * The user and subscriber have no interest in each other's presence.
+ */
+ public static final ItemType NONE = new ItemType("none");
+
+ /**
+ * The user is interested in receiving presence updates from the subscriber.
+ */
+ public static final ItemType TO = new ItemType("to");
+
+ /**
+ * The subscriber is interested in receiving presence updates from the user.
+ */
+ public static final ItemType FROM = new ItemType("from");
+
+ /**
+ * The user and subscriber have a mutual interest in each other's presence.
+ */
+ public static final ItemType BOTH = new ItemType("both");
+
+ /**
+ * The user wishes to stop receiving presence updates from the subscriber.
+ */
+ public static final ItemType REMOVE = new ItemType("remove");
+
+ public static ItemType fromString(String value) {
+ if (value == null) {
+ return null;
+ }
+ value = value.toLowerCase();
+ if ("none".equals(value)) {
+ return NONE;
+ }
+ else if ("to".equals(value)) {
+ return TO;
+ }
+ else if ("from".equals(value)) {
+ return FROM;
+ }
+ else if ("both".equals(value)) {
+ return BOTH;
+ }
+ else if ("remove".equals(value)) {
+ return REMOVE;
+ }
+ else {
+ return null;
+ }
+ }
+
+ private String value;
+
+ /**
+ * Returns the item type associated with the specified string.
+ *
+ * @param value the item type.
+ */
+ public ItemType(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Session.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Session.java
new file mode 100644
index 000000000..18cfd88cc
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Session.java
@@ -0,0 +1,45 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+/**
+ * IQ packet that will be sent to the server to establish a session.<p>
+ *
+ * If a server supports sessions, it MUST include a <i>session</i> element in the
+ * stream features it advertises to a client after the completion of stream authentication.
+ * Upon being informed that session establishment is required by the server the client MUST
+ * establish a session if it desires to engage in instant messaging and presence functionality.<p>
+ *
+ * For more information refer to the following
+ * <a href=http://www.xmpp.org/specs/rfc3921.html#session>link</a>.
+ *
+ * @author Gaston Dombiak
+ */
+public class Session extends IQ {
+
+ public Session() {
+ setType(IQ.Type.SET);
+ }
+
+ public String getChildElementXML() {
+ return "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>";
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/StreamError.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/StreamError.java
new file mode 100644
index 000000000..f6edb5450
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/StreamError.java
@@ -0,0 +1,106 @@
+/**
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2005 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+/**
+ * Represents a stream error packet. Stream errors are unrecoverable errors where the server
+ * will close the unrelying TCP connection after the stream error was sent to the client.
+ * These is the list of stream errors as defined in the XMPP spec:<p>
+ *
+ * <table border=1>
+ * <tr><td><b>Code</b></td><td><b>Description</b></td></tr>
+ * <tr><td> bad-format </td><td> the entity has sent XML that cannot be processed </td></tr>
+ * <tr><td> unsupported-encoding </td><td> the entity has sent a namespace prefix that is
+ * unsupported </td></tr>
+ * <tr><td> bad-namespace-prefix </td><td> Remote Server Timeout </td></tr>
+ * <tr><td> conflict </td><td> the server is closing the active stream for this entity
+ * because a new stream has been initiated that conflicts with the existing
+ * stream. </td></tr>
+ * <tr><td> connection-timeout </td><td> the entity has not generated any traffic over
+ * the stream for some period of time. </td></tr>
+ * <tr><td> host-gone </td><td> the value of the 'to' attribute provided by the initiating
+ * entity in the stream header corresponds to a hostname that is no longer hosted by
+ * the server. </td></tr>
+ * <tr><td> host-unknown </td><td> the value of the 'to' attribute provided by the
+ * initiating entity in the stream header does not correspond to a hostname that is
+ * hosted by the server. </td></tr>
+ * <tr><td> improper-addressing </td><td> a stanza sent between two servers lacks a 'to'
+ * or 'from' attribute </td></tr>
+ * <tr><td> internal-server-error </td><td> the server has experienced a
+ * misconfiguration. </td></tr>
+ * <tr><td> invalid-from </td><td> the JID or hostname provided in a 'from' address does
+ * not match an authorized JID. </td></tr>
+ * <tr><td> invalid-id </td><td> the stream ID or dialback ID is invalid or does not match
+ * an ID previously provided. </td></tr>
+ * <tr><td> invalid-namespace </td><td> the streams namespace name is invalid. </td></tr>
+ * <tr><td> invalid-xml </td><td> the entity has sent invalid XML over the stream. </td></tr>
+ * <tr><td> not-authorized </td><td> the entity has attempted to send data before the
+ * stream has been authenticated </td></tr>
+ * <tr><td> policy-violation </td><td> the entity has violated some local service
+ * policy. </td></tr>
+ * <tr><td> remote-connection-failed </td><td> Rthe server is unable to properly connect
+ * to a remote entity. </td></tr>
+ * <tr><td> resource-constraint </td><td> Rthe server lacks the system resources necessary
+ * to service the stream. </td></tr>
+ * <tr><td> restricted-xml </td><td> the entity has attempted to send restricted XML
+ * features. </td></tr>
+ * <tr><td> see-other-host </td><td> the server will not provide service to the initiating
+ * entity but is redirecting traffic to another host. </td></tr>
+ * <tr><td> system-shutdown </td><td> the server is being shut down and all active streams
+ * are being closed. </td></tr>
+ * <tr><td> undefined-condition </td><td> the error condition is not one of those defined
+ * by the other conditions in this list. </td></tr>
+ * <tr><td> unsupported-encoding </td><td> the initiating entity has encoded the stream in
+ * an encoding that is not supported. </td></tr>
+ * <tr><td> unsupported-stanza-type </td><td> the initiating entity has sent a first-level
+ * child of the stream that is not supported. </td></tr>
+ * <tr><td> unsupported-version </td><td> the value of the 'version' attribute provided by
+ * the initiating entity in the stream header specifies a version of XMPP that is not
+ * supported. </td></tr>
+ * <tr><td> xml-not-well-formed </td><td> the initiating entity has sent XML that is
+ * not well-formed. </td></tr>
+ * </table>
+ *
+ * @author Gaston Dombiak
+ */
+public class StreamError {
+
+ private String code;
+
+ public StreamError(String code) {
+ super();
+ this.code = code;
+ }
+
+ /**
+ * Returns the error code.
+ *
+ * @return the error code.
+ */
+ public String getCode() {
+ return code;
+ }
+
+ public String toString() {
+ StringBuffer txt = new StringBuffer();
+ txt.append("stream:error (").append(code).append(")");
+ return txt.toString();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/XMPPError.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/XMPPError.java
new file mode 100644
index 000000000..6d90c48e6
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/XMPPError.java
@@ -0,0 +1,117 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.packet;
+
+/**
+ * Represents a XMPP error sub-packet. Typically, a server responds to a request that has
+ * problems by sending the packet back and including an error packet. Each error has a code
+ * as well as as an optional text explanation. Typical error codes are as follows:<p>
+ *
+ * <table border=1>
+ * <tr><td><b>Code</b></td><td><b>Description</b></td></tr>
+ * <tr><td> 302 </td><td> Redirect </td></tr>
+ * <tr><td> 400 </td><td> Bad Request </td></tr>
+ * <tr><td> 401 </td><td> Unauthorized </td></tr>
+ * <tr><td> 402 </td><td> Payment Required </td></tr>
+ * <tr><td> 403 </td><td> Forbidden </td></tr>
+ * <tr><td> 404 </td><td> Not Found </td></tr>
+ * <tr><td> 405 </td><td> Not Allowed </td></tr>
+ * <tr><td> 406 </td><td> Not Acceptable </td></tr>
+ * <tr><td> 407 </td><td> Registration Required </td></tr>
+ * <tr><td> 408 </td><td> Request Timeout </td></tr>
+ * <tr><td> 409 </td><td> Conflict </td></tr>
+ * <tr><td> 500 </td><td> Internal Server XMPPError </td></tr>
+ * <tr><td> 501 </td><td> Not Implemented </td></tr>
+ * <tr><td> 502 </td><td> Remote Server Error </td></tr>
+ * <tr><td> 503 </td><td> Service Unavailable </td></tr>
+ * <tr><td> 504 </td><td> Remote Server Timeout </td></tr>
+ * </table>
+ *
+ * @author Matt Tucker
+ */
+public class XMPPError {
+
+ private int code;
+ private String message;
+
+ /**
+ * Creates a new error with the specified code and no message..
+ *
+ * @param code the error code.
+ */
+ public XMPPError(int code) {
+ this.code = code;
+ this.message = null;
+ }
+
+ /**
+ * Creates a new error with the specified code and message.
+ *
+ * @param code the error code.
+ * @param message a message describing the error.
+ */
+ public XMPPError(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ /**
+ * Returns the error code.
+ *
+ * @return the error code.
+ */
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * Returns the message describing the error, or null if there is no message.
+ *
+ * @return the message describing the error, or null if there is no message.
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Returns the error as XML.
+ *
+ * @return the error as XML.
+ */
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<error code=\"").append(code).append("\">");
+ if (message != null) {
+ buf.append(message);
+ }
+ buf.append("</error>");
+ return buf.toString();
+ }
+
+ public String toString() {
+ StringBuffer txt = new StringBuffer();
+ txt.append("(").append(code).append(")");
+ if (message != null) {
+ txt.append(" ").append(message);
+ }
+ return txt.toString();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/package.html
new file mode 100644
index 000000000..18a6405c8
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/package.html
@@ -0,0 +1 @@
+<body>XML packets that are part of the XMPP protocol.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/IQProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/IQProvider.java
new file mode 100644
index 000000000..8273283f1
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/IQProvider.java
@@ -0,0 +1,47 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * An interface for parsing custom IQ packets. Each IQProvider must be registered with
+ * the ProviderManager class for it to be used. Every implementation of this
+ * interface <b>must</b> have a public, no-argument constructor.
+ *
+ * @author Matt Tucker
+ */
+public interface IQProvider {
+
+ /**
+ * Parse the IQ sub-document and create an IQ instance. Each IQ must have a
+ * single child element. At the beginning of the method call, the xml parser
+ * will be positioned at the opening tag of the IQ child element. At the end
+ * of the method call, the parser <b>must</b> be positioned on the closing tag
+ * of the child element.
+ *
+ * @param parser an XML parser.
+ * @return a new IQ instance.
+ * @throws Exception if an error occurs parsing the XML.
+ */
+ public IQ parseIQ(XmlPullParser parser) throws Exception;
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java
new file mode 100644
index 000000000..40baeaafc
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java
@@ -0,0 +1,46 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * An interface for parsing custom packets extensions. Each PacketExtensionProvider must
+ * be registered with the ProviderManager class for it to be used. Every implementation
+ * of this interface <b>must</b> have a public, no-argument constructor.
+ *
+ * @author Matt Tucker
+ */
+public interface PacketExtensionProvider {
+
+ /**
+ * Parse an extension sub-packet and create a PacketExtension instance. At
+ * the beginning of the method call, the xml parser will be positioned on the
+ * opening element of the packet extension. At the end of the method call, the
+ * parser <b>must</b> be positioned on the closing element of the packet extension.
+ *
+ * @param parser an XML parser.
+ * @return a new IQ instance.
+ * @throws java.lang.Exception if an error occurs parsing the XML.
+ */
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception;
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/ProviderManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/ProviderManager.java
new file mode 100644
index 000000000..34a6c356f
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/ProviderManager.java
@@ -0,0 +1,396 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.xmlpull.v1.*;
+
+import java.util.*;
+import java.net.URL;
+
+/**
+ * Manages providers for parsing custom XML sub-documents of XMPP packets. Two types of
+ * providers exist:<ul>
+ * <li>IQProvider -- parses IQ requests into Java objects.
+ * <li>PacketExtension -- parses XML sub-documents attached to packets into
+ * PacketExtension instances.</ul>
+ *
+ * <b>IQProvider</b><p>
+ *
+ * By default, Smack only knows how to process IQ packets with sub-packets that
+ * are in a few namespaces such as:<ul>
+ * <li>jabber:iq:auth
+ * <li>jabber:iq:roster
+ * <li>jabber:iq:register</ul>
+ *
+ * Because many more IQ types are part of XMPP and its extensions, a pluggable IQ parsing
+ * mechanism is provided. IQ providers are registered programatically or by creating a
+ * smack.providers file in the META-INF directory of your JAR file. The file is an XML
+ * document that contains one or more iqProvider entries, as in the following example:
+ *
+ * <pre>
+ * &lt;?xml version="1.0"?&gt;
+ * &lt;smackProviders&gt;
+ * &lt;iqProvider&gt;
+ * &lt;elementName&gt;query&lt;/elementName&gt;
+ * &lt;namespace&gt;jabber:iq:time&lt;/namespace&gt;
+ * &lt;className&gt;org.jivesoftware.smack.packet.Time&lt/className&gt;
+ * &lt;/iqProvider&gt;
+ * &lt;/smackProviders&gt;</pre>
+ *
+ * Each IQ provider is associated with an element name and a namespace. If multiple provider
+ * entries attempt to register to handle the same namespace, the first entry loaded from the
+ * classpath will take precedence. The IQ provider class can either implement the IQProvider
+ * interface, or extend the IQ class. In the former case, each IQProvider is responsible for
+ * parsing the raw XML stream to create an IQ instance. In the latter case, bean introspection
+ * is used to try to automatically set properties of the IQ instance using the values found
+ * in the IQ packet XML. For example, an XMPP time packet resembles the following:
+ * <pre>
+ * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
+ * &lt;query xmlns='jabber:iq:time'&gt;
+ * &lt;utc&gt;20020910T17:58:35&lt;/utc&gt;
+ * &lt;tz&gt;MDT&lt;/tz&gt;
+ * &lt;display&gt;Tue Sep 10 12:58:35 2002&lt;/display&gt;
+ * &lt;/query&gt;
+ * &lt;/iq&gt;</pre>
+ *
+ * In order for this packet to be automatically mapped to the Time object listed in the
+ * providers file above, it must have the methods setUtc(String), setTz(String), and
+ * setDisplay(String). The introspection service will automatically try to convert the String
+ * value from the XML into a boolean, int, long, float, double, or Class depending on the
+ * type the IQ instance expects.<p>
+ *
+ * A pluggable system for packet extensions, child elements in a custom namespace for
+ * message and presence packets, also exists. Each extension provider
+ * is registered with a name space in the smack.providers file as in the following example:
+ *
+ * <pre>
+ * &lt;?xml version="1.0"?&gt;
+ * &lt;smackProviders&gt;
+ * &lt;extensionProvider&gt;
+ * &lt;elementName&gt;x&lt;/elementName&gt;
+ * &lt;namespace&gt;jabber:iq:event&lt;/namespace&gt;
+ * &lt;className&gt;org.jivesoftware.smack.packet.MessageEvent&lt/className&gt;
+ * &lt;/extensionProvider&gt;
+ * &lt;/smackProviders&gt;</pre>
+ *
+ * If multiple provider entries attempt to register to handle the same element name and namespace,
+ * the first entry loaded from the classpath will take precedence. Whenever a packet extension
+ * is found in a packet, parsing will be passed to the correct provider. Each provider
+ * can either implement the PacketExtensionProvider interface or be a standard Java Bean. In
+ * the former case, each extension provider is responsible for parsing the raw XML stream to
+ * contruct an object. In the latter case, bean introspection is used to try to automatically
+ * set the properties of the class using the values in the packet extension sub-element. When an
+ * extension provider is not registered for an element name and namespace combination, Smack will
+ * store all top-level elements of the sub-packet in DefaultPacketExtension object and then
+ * attach it to the packet.
+ *
+ * @author Matt Tucker
+ */
+public class ProviderManager {
+
+ private static ProviderManager defaultProvider = null;
+
+ private Map extensionProviders = new Hashtable();
+ private Map iqProviders = new Hashtable();
+
+ public static ProviderManager getDefault() {
+ if (defaultProvider == null)
+ defaultProvider = new ProviderManager();
+ return defaultProvider;
+ }
+
+ public static void setDefault(ProviderManager value) {
+ if (defaultProvider != null)
+ throw new IllegalStateException("ProviderManager default already set");
+ defaultProvider = value;
+ }
+
+ public ProviderManager() {
+ super();
+ initialize();
+ }
+
+ protected void initialize() {
+ // Load IQ processing providers.
+ try {
+ // Get an array of class loaders to try loading the providers files from.
+ ClassLoader[] classLoaders = getClassLoaders();
+ for (int i=0; i<classLoaders.length; i++) {
+ Enumeration providerEnum = classLoaders[i].getResources(
+ "META-INF/smack.providers");
+ while (providerEnum.hasMoreElements()) {
+ URL url = (URL)providerEnum.nextElement();
+ java.io.InputStream providerStream = null;
+ try {
+ providerStream = url.openStream();
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance(
+ "org.xmlpull.mxp1.MXParserFactory", null);
+ factory.setNamespaceAware(true);
+ XmlPullParser parser = factory.newPullParser();
+ parser.setInput(providerStream, "UTF-8");
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("iqProvider")) {
+ parser.next();
+ parser.next();
+ String elementName = parser.nextText();
+ parser.next();
+ parser.next();
+ String namespace = parser.nextText();
+ parser.next();
+ parser.next();
+ String className = parser.nextText();
+ // Only add the provider for the namespace if one isn't
+ // already registered.
+ String key = getProviderKey(elementName, namespace);
+ if (!iqProviders.containsKey(key)) {
+ // Attempt to load the provider class and then create
+ // a new instance if it's an IQProvider. Otherwise, if it's
+ // an IQ class, add the class object itself, then we'll use
+ // reflection later to create instances of the class.
+ try {
+ // Add the provider to the map.
+ Class provider = Class.forName(className);
+ if (IQProvider.class.isAssignableFrom(provider)) {
+ iqProviders.put(key, provider.newInstance());
+ }
+ else if (IQ.class.isAssignableFrom(provider)) {
+ iqProviders.put(key, provider);
+ }
+ }
+ catch (ClassNotFoundException cnfe) {
+ cnfe.printStackTrace();
+ }
+ }
+ }
+ else if (parser.getName().equals("extensionProvider")) {
+ parser.next();
+ parser.next();
+ String elementName = parser.nextText();
+ parser.next();
+ parser.next();
+ String namespace = parser.nextText();
+ parser.next();
+ parser.next();
+ String className = parser.nextText();
+ // Only add the provider for the namespace if one isn't
+ // already registered.
+ String key = getProviderKey(elementName, namespace);
+ if (!extensionProviders.containsKey(key)) {
+ // Attempt to load the provider class and then create
+ // a new instance if it's a Provider. Otherwise, if it's
+ // a PacketExtension, add the class object itself and
+ // then we'll use reflection later to create instances
+ // of the class.
+ try {
+ // Add the provider to the map.
+ Class provider = Class.forName(className);
+ if (PacketExtensionProvider.class.isAssignableFrom(
+ provider))
+ {
+ extensionProviders.put(key, provider.newInstance());
+ }
+ else if (PacketExtension.class.isAssignableFrom(
+ provider))
+ {
+ extensionProviders.put(key, provider);
+ }
+ }
+ catch (ClassNotFoundException cnfe) {
+ cnfe.printStackTrace();
+ }
+ }
+ }
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+ }
+ finally {
+ try { providerStream.close(); }
+ catch (Exception e) { }
+ }
+ }
+ }
+ }
+ catch (Exception e) { }
+ }
+
+ /**
+ * Returns the IQ provider registered to the specified XML element name and namespace.
+ * For example, if a provider was registered to the element name "query" and the
+ * namespace "jabber:iq:time", then the following packet would trigger the provider:
+ *
+ * <pre>
+ * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
+ * &lt;query xmlns='jabber:iq:time'&gt;
+ * &lt;utc&gt;20020910T17:58:35&lt;/utc&gt;
+ * &lt;tz&gt;MDT&lt;/tz&gt;
+ * &lt;display&gt;Tue Sep 10 12:58:35 2002&lt;/display&gt;
+ * &lt;/query&gt;
+ * &lt;/iq&gt;</pre>
+ *
+ * <p>Note: this method is generally only called by the internal Smack classes.
+ *
+ * @param elementName the XML element name.
+ * @param namespace the XML namespace.
+ * @return the IQ provider.
+ */
+ public Object getIQProvider(String elementName, String namespace) {
+ String key = getProviderKey(elementName, namespace);
+ return iqProviders.get(key);
+ }
+
+ /**
+ * Returns an Iterator for all IQProvider instances.
+ *
+ * @return an Iterator for all IQProvider instances.
+ */
+ public Iterator getIQProviders() {
+ return Collections.unmodifiableCollection(new HashMap(iqProviders).values()).iterator();
+ }
+
+ /**
+ * Adds an IQ provider (must be an instance of IQProvider or Class object that is an IQ)
+ * with the specified element name and name space. The provider will override any providers
+ * loaded through the classpath.
+ *
+ * @param elementName the XML element name.
+ * @param namespace the XML namespace.
+ * @param provider the IQ provider.
+ */
+ public void addIQProvider(String elementName, String namespace,
+ Object provider)
+ {
+ if (!(provider instanceof IQProvider || (provider instanceof Class &&
+ IQ.class.isAssignableFrom((Class)provider))))
+ {
+ throw new IllegalArgumentException("Provider must be an IQProvider " +
+ "or a Class instance.");
+ }
+ String key = getProviderKey(elementName, namespace);
+ iqProviders.put(key, provider);
+ }
+
+ /**
+ * Removes the IQ provider with the specified element name and name space.
+ *
+ * @param elementName the XML element name.
+ * @param namespace the XML namespace.
+ */
+ public void removeIQProvider(String elementName, String namespace) {
+ String key = getProviderKey(elementName, namespace);
+ iqProviders.remove(key);
+ }
+
+ /**
+ * Returns the packet extension provider registered to the specified XML element name
+ * and namespace. For example, if a provider was registered to the element name "x" and the
+ * namespace "jabber:x:event", then the following packet would trigger the provider:
+ *
+ * <pre>
+ * &lt;message to='romeo@montague.net' id='message_1'&gt;
+ * &lt;body&gt;Art thou not Romeo, and a Montague?&lt;/body&gt;
+ * &lt;x xmlns='jabber:x:event'&gt;
+ * &lt;composing/&gt;
+ * &lt;/x&gt;
+ * &lt;/message&gt;</pre>
+ *
+ * <p>Note: this method is generally only called by the internal Smack classes.
+ *
+ * @param elementName
+ * @param namespace
+ * @return the extenion provider.
+ */
+ public Object getExtensionProvider(String elementName, String namespace) {
+ String key = getProviderKey(elementName, namespace);
+ return extensionProviders.get(key);
+ }
+
+ /**
+ * Adds an extension provider with the specified element name and name space. The provider
+ * will override any providers loaded through the classpath. The provider must be either
+ * a PacketExtensionProvider instance, or a Class object of a Javabean.
+ *
+ * @param elementName the XML element name.
+ * @param namespace the XML namespace.
+ * @param provider the extension provider.
+ */
+ public void addExtensionProvider(String elementName, String namespace,
+ Object provider)
+ {
+ if (!(provider instanceof PacketExtensionProvider || provider instanceof Class)) {
+ throw new IllegalArgumentException("Provider must be a PacketExtensionProvider " +
+ "or a Class instance.");
+ }
+ String key = getProviderKey(elementName, namespace);
+ extensionProviders.put(key, provider);
+ }
+
+ /**
+ * Removes the extension provider with the specified element name and name space.
+ *
+ * @param elementName the XML element name.
+ * @param namespace the XML namespace.
+ */
+ public void removeExtensionProvider(String elementName, String namespace) {
+ String key = getProviderKey(elementName, namespace);
+ extensionProviders.remove(key);
+ }
+
+ /**
+ * Returns an Iterator for all PacketExtensionProvider instances.
+ *
+ * @return an Iterator for all PacketExtensionProvider instances.
+ */
+ public Iterator getExtensionProviders() {
+ return Collections.unmodifiableCollection(
+ new HashMap(extensionProviders).values()).iterator();
+ }
+
+ /**
+ * Returns a String key for a given element name and namespace.
+ *
+ * @param elementName the element name.
+ * @param namespace the namespace.
+ * @return a unique key for the element name and namespace pair.
+ */
+ protected static String getProviderKey(String elementName, String namespace) {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(elementName).append("/><").append(namespace).append("/>");
+ return buf.toString();
+ }
+
+ /**
+ * Returns an array of class loaders to load resources from.
+ *
+ * @return an array of ClassLoader instances.
+ */
+ private static ClassLoader[] getClassLoaders() {
+ ClassLoader[] classLoaders = new ClassLoader[3];
+ classLoaders[0] = ProviderManager.class.getClassLoader();
+ classLoaders[1] = Thread.currentThread().getContextClassLoader();
+ classLoaders[2] = ClassLoader.getSystemClassLoader();
+ return classLoaders;
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/package.html
new file mode 100644
index 000000000..fccc3836d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/package.html
@@ -0,0 +1 @@
+<body>Provides pluggable parsing of incoming IQ's and packet extensions.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLAnonymous.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLAnonymous.java
new file mode 100644
index 000000000..cdb81e08a
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLAnonymous.java
@@ -0,0 +1,52 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2005 Jive Software.
+ *
+ * All rights reserved. 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.sasl;
+
+import org.jivesoftware.smack.SASLAuthentication;
+
+/**
+ * Implementation of the SASL ANONYMOUS mechanisn as defined by the
+ * <a href="http://www.ietf.org/internet-drafts/draft-ietf-sasl-anon-05.txt">IETF draft
+ * document</a>.
+ *
+ * @author Gaston Dombiak
+ */
+public class SASLAnonymous extends SASLMechanism {
+
+ public SASLAnonymous(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+ }
+
+ protected String getName() {
+ return "ANONYMOUS";
+ }
+
+ protected String getAuthenticationText(String username, String host, String password) {
+ // Nothing to send in the <auth> body
+ return null;
+ }
+
+ protected String getChallengeResponse(byte[] bytes) {
+ // Some servers may send a challenge to gather more information such as
+ // email address. Return any string value.
+ return "anything";
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLMechanism.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLMechanism.java
new file mode 100644
index 000000000..b09176755
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLMechanism.java
@@ -0,0 +1,122 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.sasl;
+
+import org.jivesoftware.smack.SASLAuthentication;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.io.IOException;
+
+/**
+ * Base class for SASL mechanisms. Subclasses must implement three methods:
+ * <ul>
+ * <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li>
+ * <li>{@link #getAuthenticationText(String, String, String)} -- authentication text to include
+ * in the initial <tt>auth</tt> stanza.</li>
+ * <li>{@link #getChallengeResponse(byte[])} -- to respond challenges made by the server.</li>
+ * </ul>
+ *
+ * @author Gaston Dombiak
+ */
+public abstract class SASLMechanism {
+
+ private SASLAuthentication saslAuthentication;
+
+ public SASLMechanism(SASLAuthentication saslAuthentication) {
+ super();
+ this.saslAuthentication = saslAuthentication;
+ }
+
+ /**
+ * Builds and sends the <tt>auth</tt> stanza to the server.
+ *
+ * @param username the username of the user being authenticated.
+ * @param host the hostname where the user account resides.
+ * @param password the password of the user.
+ * @throws IOException If a network error occures while authenticating.
+ */
+ public void authenticate(String username, String host, String password) throws IOException {
+ // Build the authentication stanza encoding the authentication text
+ StringBuffer stanza = new StringBuffer();
+ stanza.append("<auth mechanism=\"").append(getName());
+ stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ String authenticationText = getAuthenticationText(username, host, password);
+ if (authenticationText != null) {
+ stanza.append(StringUtils.encodeBase64(authenticationText));
+ }
+ stanza.append("</auth>");
+
+ // Send the authentication to the server
+ getSASLAuthentication().send(stanza.toString());
+ }
+
+ /**
+ * The server is challenging the SASL mechanism for the stanza he just sent. Send a
+ * response to the server's challenge.
+ *
+ * @param challenge a base64 encoded string representing the challenge.
+ */
+ public void challengeReceived(String challenge) throws IOException {
+ // Build the challenge response stanza encoding the response text
+ StringBuffer stanza = new StringBuffer();
+ stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ String authenticationText = getChallengeResponse(StringUtils.decodeBase64(challenge));
+ if (authenticationText != null) {
+ stanza.append(StringUtils.encodeBase64(authenticationText));
+ }
+ stanza.append("</response>");
+
+ // Send the authentication to the server
+ getSASLAuthentication().send(stanza.toString());
+ }
+
+ /**
+ * Returns the response text to send answering the challenge sent by the server. Mechanisms
+ * that will never receive a challenge may redefine this method returning <tt>null</tt>.
+ *
+ * @param bytes the challenge sent by the server.
+ * @return the response text to send to answer the challenge sent by the server.
+ */
+ protected abstract String getChallengeResponse(byte[] bytes);
+
+ /**
+ * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ *
+ * @return the common name of the SASL mechanism.
+ */
+ protected abstract String getName();
+
+ /**
+ * Returns the authentication text to include in the initial <tt>auth</tt> stanza
+ * or <tt>null</tt> if nothing should be added.
+ *
+ * @param username the username of the user being authenticated.
+ * @param host the hostname where the user account resides.
+ * @param password the password of the user.
+ * @return the authentication text to include in the initial <tt>auth</tt> stanza
+ * or <tt>null</tt> if nothing should be added.
+ */
+ protected abstract String getAuthenticationText(String username, String host, String password);
+
+ protected SASLAuthentication getSASLAuthentication() {
+ return saslAuthentication;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java
new file mode 100644
index 000000000..54d18c607
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java
@@ -0,0 +1,58 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.sasl;
+
+import org.jivesoftware.smack.SASLAuthentication;
+
+/**
+ * Implementation of the SASL PLAIN mechanisn as defined by the
+ * <a href="http://www.ietf.org/internet-drafts/draft-ietf-sasl-plain-08.txt">IETF draft
+ * document</a>.
+ *
+ * @author Gaston Dombiak
+ */
+public class SASLPlainMechanism extends SASLMechanism {
+
+ public SASLPlainMechanism(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+ }
+
+ protected String getName() {
+ return "PLAIN";
+ }
+
+ protected String getAuthenticationText(String username, String host, String password) {
+ // Build the text containing the "authorization identity" + NUL char +
+ // "authentication identity" + NUL char + "clear-text password"
+ StringBuffer text = new StringBuffer();
+ text.append(username).append("@").append(host);
+ text.append('\0');
+ text.append(username);
+ text.append('\0');
+ text.append(password);
+ return text.toString();
+ }
+
+ protected String getChallengeResponse(byte[] bytes) {
+ // Return null since this mechanism will never get a challenge from the server
+ return null;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/package.html
new file mode 100644
index 000000000..1e8cfb784
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/package.html
@@ -0,0 +1 @@
+<body>SASL Mechanisms.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/Cache.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/Cache.java
new file mode 100644
index 000000000..fb8a3bcac
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/Cache.java
@@ -0,0 +1,628 @@
+/**
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2005 Jive Software.
+ *
+ * All rights reserved. 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.util;
+
+import java.util.*;
+
+/**
+ * A specialized Map that is size-limited (using an LRU algorithm) and
+ * has an optional expiration time for cache items. The Map is thread-safe.<p>
+ *
+ * The algorithm for cache is as follows: a HashMap is maintained for fast
+ * object lookup. Two linked lists are maintained: one keeps objects in the
+ * order they are accessed from cache, the other keeps objects in the order
+ * they were originally added to cache. When objects are added to cache, they
+ * are first wrapped by a CacheObject which maintains the following pieces
+ * of information:<ul>
+ * <li> A pointer to the node in the linked list that maintains accessed
+ * order for the object. Keeping a reference to the node lets us avoid
+ * linear scans of the linked list.
+ * <li> A pointer to the node in the linked list that maintains the age
+ * of the object in cache. Keeping a reference to the node lets us avoid
+ * linear scans of the linked list.</ul>
+ * <p/>
+ * To get an object from cache, a hash lookup is performed to get a reference
+ * to the CacheObject that wraps the real object we are looking for.
+ * The object is subsequently moved to the front of the accessed linked list
+ * and any necessary cache cleanups are performed. Cache deletion and expiration
+ * is performed as needed.
+ *
+ * @author Matt Tucker
+ */
+public class Cache implements Map {
+
+ /**
+ * The map the keys and values are stored in.
+ */
+ protected Map map;
+
+ /**
+ * Linked list to maintain order that cache objects are accessed
+ * in, most used to least used.
+ */
+ protected LinkedList lastAccessedList;
+
+ /**
+ * Linked list to maintain time that cache objects were initially added
+ * to the cache, most recently added to oldest added.
+ */
+ protected LinkedList ageList;
+
+ /**
+ * Maximum number of items the cache will hold.
+ */
+ protected int maxCacheSize;
+
+ /**
+ * Maximum length of time objects can exist in cache before expiring.
+ */
+ protected long maxLifetime;
+
+ /**
+ * Maintain the number of cache hits and misses. A cache hit occurs every
+ * time the get method is called and the cache contains the requested
+ * object. A cache miss represents the opposite occurence.<p>
+ *
+ * Keeping track of cache hits and misses lets one measure how efficient
+ * the cache is; the higher the percentage of hits, the more efficient.
+ */
+ protected long cacheHits, cacheMisses = 0L;
+
+ /**
+ * Create a new cache and specify the maximum size of for the cache in
+ * bytes, and the maximum lifetime of objects.
+ *
+ * @param maxSize the maximum number of objects the cache will hold. -1
+ * means the cache has no max size.
+ * @param maxLifetime the maximum amount of time (in ms) objects can exist in
+ * cache before being deleted. -1 means objects never expire.
+ */
+ public Cache(int maxSize, long maxLifetime) {
+ if (maxSize == 0) {
+ throw new IllegalArgumentException("Max cache size cannot be 0.");
+ }
+ this.maxCacheSize = maxSize;
+ this.maxLifetime = maxLifetime;
+
+ // Our primary data structure is a hash map. The default capacity of 11
+ // is too small in almost all cases, so we set it bigger.
+ map = new HashMap(103);
+
+ lastAccessedList = new LinkedList();
+ ageList = new LinkedList();
+ }
+
+ public synchronized Object put(Object key, Object value) {
+ Object oldValue = null;
+ // Delete an old entry if it exists.
+ if (map.containsKey(key)) {
+ oldValue = remove(key, true);
+ }
+
+ CacheObject cacheObject = new CacheObject(value);
+ map.put(key, cacheObject);
+ // Make an entry into the cache order list.
+ // Store the cache order list entry so that we can get back to it
+ // during later lookups.
+ cacheObject.lastAccessedListNode = lastAccessedList.addFirst(key);
+ // Add the object to the age list
+ LinkedListNode ageNode = ageList.addFirst(key);
+ ageNode.timestamp = System.currentTimeMillis();
+ cacheObject.ageListNode = ageNode;
+
+ // If cache is too full, remove least used cache entries until it is not too full.
+ cullCache();
+
+ return oldValue;
+ }
+
+ public synchronized Object get(Object key) {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ CacheObject cacheObject = (CacheObject) map.get(key);
+ if (cacheObject == null) {
+ // The object didn't exist in cache, so increment cache misses.
+ cacheMisses++;
+ return null;
+ }
+ // Remove the object from it's current place in the cache order list,
+ // and re-insert it at the front of the list.
+ cacheObject.lastAccessedListNode.remove();
+ lastAccessedList.addFirst(cacheObject.lastAccessedListNode);
+
+ // The object exists in cache, so increment cache hits. Also, increment
+ // the object's read count.
+ cacheHits++;
+ cacheObject.readCount++;
+
+ return cacheObject.object;
+ }
+
+ public synchronized Object remove(Object key) {
+ return remove(key, false);
+ }
+
+ /*
+ * Remove operation with a flag so we can tell coherence if the remove was
+ * caused by cache internal processing such as eviction or loading
+ */
+ public synchronized Object remove(Object key, boolean internal) {
+ CacheObject cacheObject = (CacheObject) map.remove(key);
+ // If the object is not in cache, stop trying to remove it.
+ if (cacheObject == null) {
+ return null;
+ }
+ // Remove from the cache order list
+ cacheObject.lastAccessedListNode.remove();
+ cacheObject.ageListNode.remove();
+ // Remove references to linked list nodes
+ cacheObject.ageListNode = null;
+ cacheObject.lastAccessedListNode = null;
+
+ return cacheObject.object;
+ }
+
+ public synchronized void clear() {
+ Object[] keys = map.keySet().toArray();
+ for (int i = 0; i < keys.length; i++) {
+ remove(keys[i]);
+ }
+
+ // Now, reset all containers.
+ map.clear();
+ lastAccessedList.clear();
+ ageList.clear();
+
+ cacheHits = 0;
+ cacheMisses = 0;
+ }
+
+ public synchronized int size() {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ return map.size();
+ }
+
+ public synchronized boolean isEmpty() {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ return map.isEmpty();
+ }
+
+ public synchronized Collection values() {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ Object[] cacheObjects = map.values().toArray();
+ Object[] values = new Object[cacheObjects.length];
+ for (int i = 0; i < cacheObjects.length; i++) {
+ values[i] = ((CacheObject) cacheObjects[i]).object;
+ }
+ return Collections.unmodifiableList(Arrays.asList(values));
+ }
+
+ public synchronized boolean containsKey(Object key) {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ return map.containsKey(key);
+ }
+
+ public void putAll(Map map) {
+ for (Iterator i = map.entrySet().iterator(); i.hasNext();) {
+ Map.Entry entry = (Map.Entry) i.next();
+ Object value = entry.getValue();
+ // If the map is another DefaultCache instance than the
+ // entry values will be CacheObject instances that need
+ // to be converted to the normal object form.
+ if (value instanceof CacheObject) {
+ value = ((CacheObject) value).object;
+ }
+ put(entry.getKey(), value);
+ }
+ }
+
+ public synchronized boolean containsValue(Object value) {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ CacheObject cacheObject = new CacheObject(value);
+
+ return map.containsValue(cacheObject);
+ }
+
+ public synchronized Set entrySet() {
+ // Warning -- this method returns CacheObject instances and not Objects
+ // in the same form they were put into cache.
+
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ return Collections.unmodifiableSet(map.entrySet());
+ }
+
+ public synchronized Set keySet() {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ return Collections.unmodifiableSet(map.keySet());
+ }
+
+ public long getCacheHits() {
+ return cacheHits;
+ }
+
+ public long getCacheMisses() {
+ return cacheMisses;
+ }
+
+ public int getMaxCacheSize() {
+ return maxCacheSize;
+ }
+
+ public synchronized void setMaxCacheSize(int maxCacheSize) {
+ this.maxCacheSize = maxCacheSize;
+ // It's possible that the new max size is smaller than our current cache
+ // size. If so, we need to delete infrequently used items.
+ cullCache();
+ }
+
+ public long getMaxLifetime() {
+ return maxLifetime;
+ }
+
+ public void setMaxLifetime(long maxLifetime) {
+ this.maxLifetime = maxLifetime;
+ }
+
+ /**
+ * Clears all entries out of cache where the entries are older than the
+ * maximum defined age.
+ */
+ protected synchronized void deleteExpiredEntries() {
+ // Check if expiration is turned on.
+ if (maxLifetime <= 0) {
+ return;
+ }
+
+ // Remove all old entries. To do this, we remove objects from the end
+ // of the linked list until they are no longer too old. We get to avoid
+ // any hash lookups or looking at any more objects than is strictly
+ // neccessary.
+ LinkedListNode node = ageList.getLast();
+ // If there are no entries in the age list, return.
+ if (node == null) {
+ return;
+ }
+
+ // Determine the expireTime, which is the moment in time that elements
+ // should expire from cache. Then, we can do an easy check to see
+ // if the expire time is greater than the expire time.
+ long expireTime = System.currentTimeMillis() - maxLifetime;
+
+ while (expireTime > node.timestamp) {
+ if (remove(node.object, true) == null) {
+ System.err.println("Error attempting to remove(" + node.object.toString() +
+ ") - cacheObject not found in cache!");
+ // remove from the ageList
+ node.remove();
+ }
+
+ // Get the next node.
+ node = ageList.getLast();
+ // If there are no more entries in the age list, return.
+ if (node == null) {
+ return;
+ }
+ }
+ }
+
+ /**
+ * Removes the least recently used elements if the cache size is greater than
+ * or equal to the maximum allowed size until the cache is at least 10% empty.
+ */
+ protected synchronized void cullCache() {
+ // Check if a max cache size is defined.
+ if (maxCacheSize < 0) {
+ return;
+ }
+
+ // See if the cache is too big. If so, clean out cache until it's 10% free.
+ if (map.size() > maxCacheSize) {
+ // First, delete any old entries to see how much memory that frees.
+ deleteExpiredEntries();
+ // Next, delete the least recently used elements until 10% of the cache
+ // has been freed.
+ int desiredSize = (int) (maxCacheSize * .90);
+ for (int i=map.size(); i>desiredSize; i--) {
+ // Get the key and invoke the remove method on it.
+ if (remove(lastAccessedList.getLast().object, true) == null) {
+ System.err.println("Error attempting to cullCache with remove(" +
+ lastAccessedList.getLast().object.toString() + ") - " +
+ "cacheObject not found in cache!");
+ lastAccessedList.getLast().remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * Wrapper for all objects put into cache. It's primary purpose is to maintain
+ * references to the linked lists that maintain the creation time of the object
+ * and the ordering of the most used objects.
+ *
+ * This class is optimized for speed rather than strictly correct encapsulation.
+ */
+ private static class CacheObject {
+
+ /**
+ * Underlying object wrapped by the CacheObject.
+ */
+ public Object object;
+
+ /**
+ * A reference to the node in the cache order list. We keep the reference
+ * here to avoid linear scans of the list. Every time the object is
+ * accessed, the node is removed from its current spot in the list and
+ * moved to the front.
+ */
+ public LinkedListNode lastAccessedListNode;
+
+ /**
+ * A reference to the node in the age order list. We keep the reference
+ * here to avoid linear scans of the list. The reference is used if the
+ * object has to be deleted from the list.
+ */
+ public LinkedListNode ageListNode;
+
+ /**
+ * A count of the number of times the object has been read from cache.
+ */
+ public int readCount = 0;
+
+ /**
+ * Creates a new cache object wrapper.
+ *
+ * @param object the underlying Object to wrap.
+ */
+ public CacheObject(Object object) {
+ this.object = object;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof CacheObject)) {
+ return false;
+ }
+
+ final CacheObject cacheObject = (CacheObject) o;
+
+ if (!object.equals(cacheObject.object)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public int hashCode() {
+ return object.hashCode();
+ }
+ }
+
+ /**
+ * Simple LinkedList implementation. The main feature is that list nodes
+ * are public, which allows very fast delete operations when one has a
+ * reference to the node that is to be deleted.<p>
+ */
+ private static class LinkedList {
+
+ /**
+ * The root of the list keeps a reference to both the first and last
+ * elements of the list.
+ */
+ private LinkedListNode head = new LinkedListNode("head", null, null);
+
+ /**
+ * Creates a new linked list.
+ */
+ public LinkedList() {
+ head.next = head.previous = head;
+ }
+
+ /**
+ * Returns the first linked list node in the list.
+ *
+ * @return the first element of the list.
+ */
+ public LinkedListNode getFirst() {
+ LinkedListNode node = head.next;
+ if (node == head) {
+ return null;
+ }
+ return node;
+ }
+
+ /**
+ * Returns the last linked list node in the list.
+ *
+ * @return the last element of the list.
+ */
+ public LinkedListNode getLast() {
+ LinkedListNode node = head.previous;
+ if (node == head) {
+ return null;
+ }
+ return node;
+ }
+
+ /**
+ * Adds a node to the beginning of the list.
+ *
+ * @param node the node to add to the beginning of the list.
+ */
+ public LinkedListNode addFirst(LinkedListNode node) {
+ node.next = head.next;
+ node.previous = head;
+ node.previous.next = node;
+ node.next.previous = node;
+ return node;
+ }
+
+ /**
+ * Adds an object to the beginning of the list by automatically creating a
+ * a new node and adding it to the beginning of the list.
+ *
+ * @param object the object to add to the beginning of the list.
+ * @return the node created to wrap the object.
+ */
+ public LinkedListNode addFirst(Object object) {
+ LinkedListNode node = new LinkedListNode(object, head.next, head);
+ node.previous.next = node;
+ node.next.previous = node;
+ return node;
+ }
+
+ /**
+ * Adds an object to the end of the list by automatically creating a
+ * a new node and adding it to the end of the list.
+ *
+ * @param object the object to add to the end of the list.
+ * @return the node created to wrap the object.
+ */
+ public LinkedListNode addLast(Object object) {
+ LinkedListNode node = new LinkedListNode(object, head, head.previous);
+ node.previous.next = node;
+ node.next.previous = node;
+ return node;
+ }
+
+ /**
+ * Erases all elements in the list and re-initializes it.
+ */
+ public void clear() {
+ //Remove all references in the list.
+ LinkedListNode node = getLast();
+ while (node != null) {
+ node.remove();
+ node = getLast();
+ }
+
+ //Re-initialize.
+ head.next = head.previous = head;
+ }
+
+ /**
+ * Returns a String representation of the linked list with a comma
+ * delimited list of all the elements in the list.
+ *
+ * @return a String representation of the LinkedList.
+ */
+ public String toString() {
+ LinkedListNode node = head.next;
+ StringBuffer buf = new StringBuffer();
+ while (node != head) {
+ buf.append(node.toString()).append(", ");
+ node = node.next;
+ }
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Doubly linked node in a LinkedList. Most LinkedList implementations keep the
+ * equivalent of this class private. We make it public so that references
+ * to each node in the list can be maintained externally.
+ *
+ * Exposing this class lets us make remove operations very fast. Remove is
+ * built into this class and only requires two reference reassignments. If
+ * remove existed in the main LinkedList class, a linear scan would have to
+ * be performed to find the correct node to delete.
+ *
+ * The linked list implementation was specifically written for the Jive
+ * cache system. While it can be used as a general purpose linked list, for
+ * most applications, it is more suitable to use the linked list that is part
+ * of the Java Collections package.
+ */
+ private static class LinkedListNode {
+
+ public LinkedListNode previous;
+ public LinkedListNode next;
+ public Object object;
+
+ /**
+ * This class is further customized for the Jive cache system. It
+ * maintains a timestamp of when a Cacheable object was first added to
+ * cache. Timestamps are stored as long values and represent the number
+ * of milliseconds passed since January 1, 1970 00:00:00.000 GMT.<p>
+ *
+ * The creation timestamp is used in the case that the cache has a
+ * maximum lifetime set. In that case, when
+ * [current time] - [creation time] > [max lifetime], the object will be
+ * deleted from cache.
+ */
+ public long timestamp;
+
+ /**
+ * Constructs a new linked list node.
+ *
+ * @param object the Object that the node represents.
+ * @param next a reference to the next LinkedListNode in the list.
+ * @param previous a reference to the previous LinkedListNode in the list.
+ */
+ public LinkedListNode(Object object, LinkedListNode next,
+ LinkedListNode previous)
+ {
+ this.object = object;
+ this.next = next;
+ this.previous = previous;
+ }
+
+ /**
+ * Removes this node from the linked list that it is a part of.
+ */
+ public void remove() {
+ previous.next = next;
+ next.previous = previous;
+ }
+
+ /**
+ * Returns a String representation of the linked list node by calling the
+ * toString method of the node's object.
+ *
+ * @return a String representation of the LinkedListNode.
+ */
+ public String toString() {
+ return object.toString();
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/DNSUtil.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/DNSUtil.java
new file mode 100644
index 000000000..bf0d7cf3f
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/DNSUtil.java
@@ -0,0 +1,219 @@
+/**
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2005 Jive Software.
+ *
+ * All rights reserved. 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.util;
+
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.Attributes;
+import java.util.Hashtable;
+import java.util.Map;
+
+/**
+ * Utilty class to perform DNS lookups for XMPP services.
+ *
+ * @author Matt Tucker
+ */
+public class DNSUtil {
+
+ /**
+ * Create a cache to hold the 100 most recently accessed DNS lookups for a period of
+ * 10 minutes.
+ */
+ private static Map cache = new Cache(100, 1000*60*10);
+
+ private static DirContext context;
+
+ static {
+ try {
+ Hashtable env = new Hashtable();
+ env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
+ context = new InitialDirContext(env);
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+ }
+
+ /**
+ * Returns the host name and port that the specified XMPP server can be
+ * reached at for client-to-server communication. A DNS lookup for a SRV
+ * record in the form "_xmpp-client._tcp.example.com" is attempted, according
+ * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form
+ * of "_jabber._tcp.example.com" is attempted since servers that implement an
+ * older version of the protocol may be listed using that notation. If that
+ * lookup fails as well, it's assumed that the XMPP server lives at the
+ * host resolved by a DNS lookup at the specified domain on the default port
+ * of 5222.<p>
+ *
+ * As an example, a lookup for "example.com" may return "im.example.com:5269".
+ *
+ * @param domain the domain.
+ * @return a HostAddress, which encompasses the hostname and port that the XMPP
+ * server can be reached at for the specified domain.
+ */
+ public static HostAddress resolveXMPPDomain(String domain) {
+ if (context == null) {
+ return new HostAddress(domain, 5222);
+ }
+ String key = "c" + domain;
+ // Return item from cache if it exists.
+ if (cache.containsKey(key)) {
+ HostAddress address = (HostAddress)cache.get(key);
+ if (address != null) {
+ return address;
+ }
+ }
+ String host = domain;
+ int port = 5222;
+ try {
+ Attributes dnsLookup = context.getAttributes("_xmpp-client._tcp." + domain);
+ String srvRecord = (String)dnsLookup.get("SRV").get();
+ String [] srvRecordEntries = srvRecord.split(" ");
+ port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]);
+ host = srvRecordEntries[srvRecordEntries.length-1];
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+ // Host entries in DNS should end with a ".".
+ if (host.endsWith(".")) {
+ host = host.substring(0, host.length()-1);
+ }
+ HostAddress address = new HostAddress(host, port);
+ // Add item to cache.
+ cache.put(key, address);
+ return address;
+ }
+
+ /**
+ * Returns the host name and port that the specified XMPP server can be
+ * reached at for server-to-server communication. A DNS lookup for a SRV
+ * record in the form "_xmpp-server._tcp.example.com" is attempted, according
+ * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form
+ * of "_jabber._tcp.example.com" is attempted since servers that implement an
+ * older version of the protocol may be listed using that notation. If that
+ * lookup fails as well, it's assumed that the XMPP server lives at the
+ * host resolved by a DNS lookup at the specified domain on the default port
+ * of 5269.<p>
+ *
+ * As an example, a lookup for "example.com" may return "im.example.com:5269".
+ *
+ * @param domain the domain.
+ * @return a HostAddress, which encompasses the hostname and port that the XMPP
+ * server can be reached at for the specified domain.
+ */
+ public static HostAddress resolveXMPPServerDomain(String domain) {
+ if (context == null) {
+ return new HostAddress(domain, 5269);
+ }
+ String key = "s" + domain;
+ // Return item from cache if it exists.
+ if (cache.containsKey(key)) {
+ HostAddress address = (HostAddress)cache.get(key);
+ if (address != null) {
+ return address;
+ }
+ }
+ String host = domain;
+ int port = 5269;
+ try {
+ Attributes dnsLookup = context.getAttributes("_xmpp-server._tcp." + domain);
+ String srvRecord = (String)dnsLookup.get("SRV").get();
+ String [] srvRecordEntries = srvRecord.split(" ");
+ port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]);
+ host = srvRecordEntries[srvRecordEntries.length-1];
+ }
+ catch (Exception e) {
+ // Attempt lookup with older "jabber" name.
+ try {
+ Attributes dnsLookup = context.getAttributes("_jabber._tcp." + domain);
+ String srvRecord = (String)dnsLookup.get("SRV").get();
+ String [] srvRecordEntries = srvRecord.split(" ");
+ port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]);
+ host = srvRecordEntries[srvRecordEntries.length-1];
+ }
+ catch (Exception e2) { }
+ }
+ // Host entries in DNS should end with a ".".
+ if (host.endsWith(".")) {
+ host = host.substring(0, host.length()-1);
+ }
+ HostAddress address = new HostAddress(host, port);
+ // Add item to cache.
+ cache.put(key, address);
+ return address;
+ }
+
+ /**
+ * Encapsulates a hostname and port.
+ */
+ public static class HostAddress {
+
+ private String host;
+ private int port;
+
+ private HostAddress(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ /**
+ * Returns the hostname.
+ *
+ * @return the hostname.
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * Returns the port.
+ *
+ * @return the port.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ public String toString() {
+ return host + ":" + port;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof HostAddress)) {
+ return false;
+ }
+
+ final HostAddress address = (HostAddress) o;
+
+ if (!host.equals(address.host)) {
+ return false;
+ }
+ if (port != address.port) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableReader.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableReader.java
new file mode 100644
index 000000000..5b073b830
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableReader.java
@@ -0,0 +1,118 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.util;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * An ObservableReader is a wrapper on a Reader that notifies to its listeners when
+ * reading character streams.
+ *
+ * @author Gaston Dombiak
+ */
+public class ObservableReader extends Reader {
+
+ Reader wrappedReader = null;
+ List listeners = new ArrayList();
+
+ public ObservableReader(Reader wrappedReader) {
+ this.wrappedReader = wrappedReader;
+ }
+
+ public int read(char[] cbuf, int off, int len) throws IOException {
+ int count = wrappedReader.read(cbuf, off, len);
+ if (count > 0) {
+ String str = new String(cbuf, off, count);
+ // Notify that a new string has been read
+ ReaderListener[] readerListeners = null;
+ synchronized (listeners) {
+ readerListeners = new ReaderListener[listeners.size()];
+ listeners.toArray(readerListeners);
+ }
+ for (int i = 0; i < readerListeners.length; i++) {
+ readerListeners[i].read(str);
+ }
+ }
+ return count;
+ }
+
+ public void close() throws IOException {
+ wrappedReader.close();
+ }
+
+ public int read() throws IOException {
+ return wrappedReader.read();
+ }
+
+ public int read(char cbuf[]) throws IOException {
+ return wrappedReader.read(cbuf);
+ }
+
+ public long skip(long n) throws IOException {
+ return wrappedReader.skip(n);
+ }
+
+ public boolean ready() throws IOException {
+ return wrappedReader.ready();
+ }
+
+ public boolean markSupported() {
+ return wrappedReader.markSupported();
+ }
+
+ public void mark(int readAheadLimit) throws IOException {
+ wrappedReader.mark(readAheadLimit);
+ }
+
+ public void reset() throws IOException {
+ wrappedReader.reset();
+ }
+
+ /**
+ * Adds a reader listener to this reader that will be notified when
+ * new strings are read.
+ *
+ * @param readerListener a reader listener.
+ */
+ public void addReaderListener(ReaderListener readerListener) {
+ if (readerListener == null) {
+ return;
+ }
+ synchronized (listeners) {
+ if (!listeners.contains(readerListener)) {
+ listeners.add(readerListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a reader listener from this reader.
+ *
+ * @param readerListener a reader listener.
+ */
+ public void removeReaderListener(ReaderListener readerListener) {
+ synchronized (listeners) {
+ listeners.remove(readerListener);
+ }
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableWriter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableWriter.java
new file mode 100644
index 000000000..ea1c0356e
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableWriter.java
@@ -0,0 +1,120 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.util;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * An ObservableWriter is a wrapper on a Writer that notifies to its listeners when
+ * writing to character streams.
+ *
+ * @author Gaston Dombiak
+ */
+public class ObservableWriter extends Writer {
+
+ Writer wrappedWriter = null;
+ List listeners = new ArrayList();
+
+ public ObservableWriter(Writer wrappedWriter) {
+ this.wrappedWriter = wrappedWriter;
+ }
+
+ public void write(char cbuf[], int off, int len) throws IOException {
+ wrappedWriter.write(cbuf, off, len);
+ String str = new String(cbuf, off, len);
+ notifyListeners(str);
+ }
+
+ public void flush() throws IOException {
+ wrappedWriter.flush();
+ }
+
+ public void close() throws IOException {
+ wrappedWriter.close();
+ }
+
+ public void write(int c) throws IOException {
+ wrappedWriter.write(c);
+ }
+
+ public void write(char cbuf[]) throws IOException {
+ wrappedWriter.write(cbuf);
+ String str = new String(cbuf);
+ notifyListeners(str);
+ }
+
+ public void write(String str) throws IOException {
+ wrappedWriter.write(str);
+ notifyListeners(str);
+ }
+
+ public void write(String str, int off, int len) throws IOException {
+ wrappedWriter.write(str, off, len);
+ str = str.substring(off, off + len);
+ notifyListeners(str);
+ }
+
+ /**
+ * Notify that a new string has been written.
+ *
+ * @param str the written String to notify
+ */
+ private void notifyListeners(String str) {
+ WriterListener[] writerListeners = null;
+ synchronized (listeners) {
+ writerListeners = new WriterListener[listeners.size()];
+ listeners.toArray(writerListeners);
+ }
+ for (int i = 0; i < writerListeners.length; i++) {
+ writerListeners[i].write(str);
+ }
+ }
+
+ /**
+ * Adds a writer listener to this writer that will be notified when
+ * new strings are sent.
+ *
+ * @param writerListener a writer listener.
+ */
+ public void addWriterListener(WriterListener writerListener) {
+ if (writerListener == null) {
+ return;
+ }
+ synchronized (listeners) {
+ if (!listeners.contains(writerListener)) {
+ listeners.add(writerListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a writer listener from this writer.
+ *
+ * @param writerListener a writer listener.
+ */
+ public void removeWriterListener(WriterListener writerListener) {
+ synchronized (listeners) {
+ listeners.remove(writerListener);
+ }
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/PacketParserUtils.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/PacketParserUtils.java
new file mode 100644
index 000000000..842c0a34d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/PacketParserUtils.java
@@ -0,0 +1,417 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.util;
+
+import java.beans.PropertyDescriptor;
+import java.util.Map;
+import java.util.Iterator;
+import java.util.HashMap;
+import java.io.ObjectInputStream;
+import java.io.ByteArrayInputStream;
+
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.provider.ProviderManager;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Utility class that helps to parse packets. Any parsing packets method that must be shared
+ * between many clients must be placed in this utility class.
+ *
+ * @author Gaston Dombiak
+ */
+public class PacketParserUtils {
+
+ /**
+ * Namespace used to store packet properties.
+ */
+ private static final String PROPERTIES_NAMESPACE =
+ "http://www.jivesoftware.com/xmlns/xmpp/properties";
+
+ /**
+ * Parses a message packet.
+ *
+ * @param parser the XML parser, positioned at the start of a message packet.
+ * @return a Message packet.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ public static Packet parseMessage(XmlPullParser parser) throws Exception {
+ Message message = new Message();
+ String id = parser.getAttributeValue("", "id");
+ message.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id);
+ message.setTo(parser.getAttributeValue("", "to"));
+ message.setFrom(parser.getAttributeValue("", "from"));
+ message.setType(Message.Type.fromString(parser.getAttributeValue("", "type")));
+
+ // Parse sub-elements. We include extra logic to make sure the values
+ // are only read once. This is because it's possible for the names to appear
+ // in arbitrary sub-elements.
+ boolean done = false;
+ String subject = null;
+ String body = null;
+ String thread = null;
+ Map properties = null;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ String elementName = parser.getName();
+ String namespace = parser.getNamespace();
+ if (elementName.equals("subject")) {
+ if (subject == null) {
+ subject = parser.nextText();
+ }
+ }
+ else if (elementName.equals("body")) {
+ if (body == null) {
+ body = parser.nextText();
+ }
+ }
+ else if (elementName.equals("thread")) {
+ if (thread == null) {
+ thread = parser.nextText();
+ }
+ }
+ else if (elementName.equals("error")) {
+ message.setError(parseError(parser));
+ }
+ else if (elementName.equals("properties") &&
+ namespace.equals(PROPERTIES_NAMESPACE))
+ {
+ properties = parseProperties(parser);
+ }
+ // Otherwise, it must be a packet extension.
+ else {
+ message.addExtension(
+ PacketParserUtils.parsePacketExtension(elementName, namespace, parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("message")) {
+ done = true;
+ }
+ }
+ }
+ message.setSubject(subject);
+ message.setBody(body);
+ message.setThread(thread);
+ // Set packet properties.
+ if (properties != null) {
+ for (Iterator i=properties.keySet().iterator(); i.hasNext(); ) {
+ String name = (String)i.next();
+ message.setProperty(name, properties.get(name));
+ }
+ }
+ return message;
+ }
+
+ /**
+ * Parses a presence packet.
+ *
+ * @param parser the XML parser, positioned at the start of a presence packet.
+ * @return a Presence packet.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ public static Presence parsePresence(XmlPullParser parser) throws Exception {
+ Presence.Type type = Presence.Type.fromString(parser.getAttributeValue("", "type"));
+
+ Presence presence = new Presence(type);
+ presence.setTo(parser.getAttributeValue("", "to"));
+ presence.setFrom(parser.getAttributeValue("", "from"));
+ String id = parser.getAttributeValue("", "id");
+ presence.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id);
+
+ // Parse sub-elements
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ String elementName = parser.getName();
+ String namespace = parser.getNamespace();
+ if (elementName.equals("status")) {
+ presence.setStatus(parser.nextText());
+ }
+ else if (elementName.equals("priority")) {
+ try {
+ int priority = Integer.parseInt(parser.nextText());
+ presence.setPriority(priority);
+ }
+ catch (NumberFormatException nfe) { }
+ catch (IllegalArgumentException iae) {
+ // Presence priority is out of range so assume priority to be zero
+ presence.setPriority(0);
+ }
+ }
+ else if (elementName.equals("show")) {
+ presence.setMode(Presence.Mode.fromString(parser.nextText()));
+ }
+ else if (elementName.equals("error")) {
+ presence.setError(parseError(parser));
+ }
+ else if (elementName.equals("properties") &&
+ namespace.equals(PROPERTIES_NAMESPACE))
+ {
+ Map properties = parseProperties(parser);
+ // Set packet properties.
+ for (Iterator i=properties.keySet().iterator(); i.hasNext(); ) {
+ String name = (String)i.next();
+ presence.setProperty(name, properties.get(name));
+ }
+ }
+ // Otherwise, it must be a packet extension.
+ else {
+ presence.addExtension(
+ PacketParserUtils.parsePacketExtension(elementName, namespace, parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("presence")) {
+ done = true;
+ }
+ }
+ }
+ return presence;
+ }
+
+ /**
+ * Parse a properties sub-packet. If any errors occur while de-serializing Java object
+ * properties, an exception will be printed and not thrown since a thrown
+ * exception will shut down the entire connection. ClassCastExceptions will occur
+ * when both the sender and receiver of the packet don't have identical versions
+ * of the same class.
+ *
+ * @param parser the XML parser, positioned at the start of a properties sub-packet.
+ * @return a map of the properties.
+ * @throws Exception if an error occurs while parsing the properties.
+ */
+ public static Map parseProperties(XmlPullParser parser) throws Exception {
+ Map properties = new HashMap();
+ while (true) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG && parser.getName().equals("property")) {
+ // Parse a property
+ boolean done = false;
+ String name = null;
+ String type = null;
+ String valueText = null;
+ Object value = null;
+ while (!done) {
+ eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ String elementName = parser.getName();
+ if (elementName.equals("name")) {
+ name = parser.nextText();
+ }
+ else if (elementName.equals("value")) {
+ type = parser.getAttributeValue("", "type");
+ valueText = parser.nextText();
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("property")) {
+ if ("integer".equals(type)) {
+ value = new Integer(valueText);
+ }
+ else if ("long".equals(type)) {
+ value = new Long(valueText);
+ }
+ else if ("float".equals(type)) {
+ value = new Float(valueText);
+ }
+ else if ("double".equals(type)) {
+ value = new Double(valueText);
+ }
+ else if ("boolean".equals(type)) {
+ value = new Boolean(valueText);
+ }
+ else if ("string".equals(type)) {
+ value = valueText;
+ }
+ else if ("java-object".equals(type)) {
+ try {
+ byte [] bytes = StringUtils.decodeBase64(valueText);
+ ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
+ value = in.readObject();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ if (name != null && value != null) {
+ properties.put(name, value);
+ }
+ done = true;
+ }
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("properties")) {
+ break;
+ }
+ }
+ }
+ return properties;
+ }
+
+ /**
+ * Parses error sub-packets.
+ *
+ * @param parser the XML parser.
+ * @return an error sub-packet.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ public static XMPPError parseError(XmlPullParser parser) throws Exception {
+ String errorCode = "-1";
+ String message = null;
+ for (int i=0; i<parser.getAttributeCount(); i++) {
+ if (parser.getAttributeName(i).equals("code")) {
+ errorCode = parser.getAttributeValue("", "code");
+ }
+ }
+ // Get the error text in a safe way since we are not sure about the error message format
+ try {
+ message = parser.nextText();
+ }
+ catch (XmlPullParserException ex) {}
+ while (true) {
+ if (parser.getEventType() == XmlPullParser.END_TAG && parser.getName().equals("error")) {
+ break;
+ }
+ parser.next();
+ }
+ return new XMPPError(Integer.parseInt(errorCode), message);
+ }
+
+ /**
+ * Parses a packet extension sub-packet.
+ *
+ * @param elementName the XML element name of the packet extension.
+ * @param namespace the XML namespace of the packet extension.
+ * @param parser the XML parser, positioned at the starting element of the extension.
+ * @return a PacketExtension.
+ * @throws Exception if a parsing error occurs.
+ */
+ public static PacketExtension parsePacketExtension(String elementName, String namespace, XmlPullParser parser)
+ throws Exception
+ {
+ // See if a provider is registered to handle the extension.
+ Object provider = ProviderManager.getDefault().getExtensionProvider(elementName, namespace);
+ if (provider != null) {
+ if (provider instanceof PacketExtensionProvider) {
+ return ((PacketExtensionProvider)provider).parseExtension(parser);
+ }
+ else if (provider instanceof Class) {
+ return (PacketExtension)parseWithIntrospection(
+ elementName, (Class)provider, parser);
+ }
+ }
+ // No providers registered, so use a default extension.
+ DefaultPacketExtension extension = new DefaultPacketExtension(elementName, namespace);
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ // If an empty element, set the value with the empty string.
+ if (parser.isEmptyElementTag()) {
+ extension.setValue(name,"");
+ }
+ // Otherwise, get the the element text.
+ else {
+ eventType = parser.next();
+ if (eventType == XmlPullParser.TEXT) {
+ String value = parser.getText();
+ extension.setValue(name, value);
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(elementName)) {
+ done = true;
+ }
+ }
+ }
+ return extension;
+ }
+
+ public static Object parseWithIntrospection(String elementName,
+ Class objectClass, XmlPullParser parser) throws Exception
+ {
+ boolean done = false;
+ Object object = objectClass.newInstance();
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ String stringValue = parser.nextText();
+ PropertyDescriptor descriptor = new PropertyDescriptor(name, objectClass);
+ // Load the class type of the property.
+ Class propertyType = descriptor.getPropertyType();
+ // Get the value of the property by converting it from a
+ // String to the correct object type.
+ Object value = decode(propertyType, stringValue);
+ // Set the value of the bean.
+ descriptor.getWriteMethod().invoke(object, new Object[] { value });
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(elementName)) {
+ done = true;
+ }
+ }
+ }
+ return object;
+ }
+
+ /**
+ * Decodes a String into an object of the specified type. If the object
+ * type is not supported, null will be returned.
+ *
+ * @param type the type of the property.
+ * @param value the encode String value to decode.
+ * @return the String value decoded into the specified type.
+ */
+ private static Object decode(Class type, String value) throws Exception {
+ if (type.getName().equals("java.lang.String")) {
+ return value;
+ }
+ if (type.getName().equals("boolean")) {
+ return Boolean.valueOf(value);
+ }
+ if (type.getName().equals("int")) {
+ return Integer.valueOf(value);
+ }
+ if (type.getName().equals("long")) {
+ return Long.valueOf(value);
+ }
+ if (type.getName().equals("float")) {
+ return Float.valueOf(value);
+ }
+ if (type.getName().equals("double")) {
+ return Double.valueOf(value);
+ }
+ if (type.getName().equals("java.lang.Class")) {
+ return Class.forName(value);
+ }
+ return null;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ReaderListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ReaderListener.java
new file mode 100644
index 000000000..1b818e9b3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ReaderListener.java
@@ -0,0 +1,41 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.util;
+
+/**
+ * Interface that allows for implementing classes to listen for string reading
+ * events. Listeners are registered with ObservableReader objects.
+ *
+ * @see ObservableReader#addReaderListener
+ * @see ObservableReader#removeReaderListener
+ *
+ * @author Gaston Dombiak
+ */
+public interface ReaderListener {
+
+ /**
+ * Notification that the Reader has read a new string.
+ *
+ * @param str the read String
+ */
+ public abstract void read(String str);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/StringUtils.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/StringUtils.java
new file mode 100644
index 000000000..b8a32959c
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/StringUtils.java
@@ -0,0 +1,432 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.util;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.io.UnsupportedEncodingException;
+import java.util.Random;
+
+/**
+ * A collection of utility methods for String objects.
+ */
+public class StringUtils {
+
+ private static final char[] QUOTE_ENCODE = "&quot;".toCharArray();
+ private static final char[] AMP_ENCODE = "&amp;".toCharArray();
+ private static final char[] LT_ENCODE = "&lt;".toCharArray();
+ private static final char[] GT_ENCODE = "&gt;".toCharArray();
+
+ /**
+ * Returns the name portion of a XMPP address. For example, for the
+ * address "matt@jivesoftware.com/Smack", "matt" would be returned. If no
+ * username is present in the address, the empty string will be returned.
+ *
+ * @param XMPPAddress the XMPP address.
+ * @return the name portion of the XMPP address.
+ */
+ public static String parseName(String XMPPAddress) {
+ if (XMPPAddress == null) {
+ return null;
+ }
+ int atIndex = XMPPAddress.indexOf("@");
+ if (atIndex <= 0) {
+ return "";
+ }
+ else {
+ return XMPPAddress.substring(0, atIndex);
+ }
+ }
+
+ /**
+ * Returns the server portion of a XMPP address. For example, for the
+ * address "matt@jivesoftware.com/Smack", "jivesoftware.com" would be returned.
+ * If no server is present in the address, the empty string will be returned.
+ *
+ * @param XMPPAddress the XMPP address.
+ * @return the server portion of the XMPP address.
+ */
+ public static String parseServer(String XMPPAddress) {
+ if (XMPPAddress == null) {
+ return null;
+ }
+ int atIndex = XMPPAddress.indexOf("@");
+ // If the String ends with '@', return the empty string.
+ if (atIndex + 1 > XMPPAddress.length()) {
+ return "";
+ }
+ int slashIndex = XMPPAddress.indexOf("/");
+ if (slashIndex > 0) {
+ return XMPPAddress.substring(atIndex + 1, slashIndex);
+ }
+ else {
+ return XMPPAddress.substring(atIndex + 1);
+ }
+ }
+
+ /**
+ * Returns the resource portion of a XMPP address. For example, for the
+ * address "matt@jivesoftware.com/Smack", "Smack" would be returned. If no
+ * resource is present in the address, the empty string will be returned.
+ *
+ * @param XMPPAddress the XMPP address.
+ * @return the resource portion of the XMPP address.
+ */
+ public static String parseResource(String XMPPAddress) {
+ if (XMPPAddress == null) {
+ return null;
+ }
+ int slashIndex = XMPPAddress.indexOf("/");
+ if (slashIndex + 1 > XMPPAddress.length() || slashIndex < 0) {
+ return "";
+ }
+ else {
+ return XMPPAddress.substring(slashIndex + 1);
+ }
+ }
+
+ /**
+ * Returns the XMPP address with any resource information removed. For example,
+ * for the address "matt@jivesoftware.com/Smack", "matt@jivesoftware.com" would
+ * be returned.
+ *
+ * @param XMPPAddress the XMPP address.
+ * @return the bare XMPP address without resource information.
+ */
+ public static String parseBareAddress(String XMPPAddress) {
+ if (XMPPAddress == null) {
+ return null;
+ }
+ int slashIndex = XMPPAddress.indexOf("/");
+ if (slashIndex < 0) {
+ return XMPPAddress;
+ }
+ else if (slashIndex == 0) {
+ return "";
+ }
+ else {
+ return XMPPAddress.substring(0, slashIndex);
+ }
+ }
+
+ /**
+ * Escapes all necessary characters in the String so that it can be used
+ * in an XML doc.
+ *
+ * @param string the string to escape.
+ * @return the string with appropriate characters escaped.
+ */
+ public static String escapeForXML(String string) {
+ if (string == null) {
+ return null;
+ }
+ char ch;
+ int i=0;
+ int last=0;
+ char[] input = string.toCharArray();
+ int len = input.length;
+ StringBuffer out = new StringBuffer((int)(len*1.3));
+ for (; i < len; i++) {
+ ch = input[i];
+ if (ch > '>') {
+ continue;
+ }
+ else if (ch == '<') {
+ if (i > last) {
+ out.append(input, last, i - last);
+ }
+ last = i + 1;
+ out.append(LT_ENCODE);
+ }
+ else if (ch == '>') {
+ if (i > last) {
+ out.append(input, last, i - last);
+ }
+ last = i + 1;
+ out.append(GT_ENCODE);
+ }
+
+ else if (ch == '&') {
+ if (i > last) {
+ out.append(input, last, i - last);
+ }
+ // Do nothing if the string is of the form &#235; (unicode value)
+ if (!(len > i + 5
+ && input[i + 1] == '#'
+ && Character.isDigit(input[i + 2])
+ && Character.isDigit(input[i + 3])
+ && Character.isDigit(input[i + 4])
+ && input[i + 5] == ';')) {
+ last = i + 1;
+ out.append(AMP_ENCODE);
+ }
+ }
+ else if (ch == '"') {
+ if (i > last) {
+ out.append(input, last, i - last);
+ }
+ last = i + 1;
+ out.append(QUOTE_ENCODE);
+ }
+ }
+ if (last == 0) {
+ return string;
+ }
+ if (i > last) {
+ out.append(input, last, i - last);
+ }
+ return out.toString();
+ }
+
+ /**
+ * Used by the hash method.
+ */
+ private static MessageDigest digest = null;
+
+ /**
+ * Hashes a String using the SHA-1 algorithm and returns the result as a
+ * String of hexadecimal numbers. This method is synchronized to avoid
+ * excessive MessageDigest object creation. If calling this method becomes
+ * a bottleneck in your code, you may wish to maintain a pool of
+ * MessageDigest objects instead of using this method.
+ * <p>
+ * A hash is a one-way function -- that is, given an
+ * input, an output is easily computed. However, given the output, the
+ * input is almost impossible to compute. This is useful for passwords
+ * since we can store the hash and a hacker will then have a very hard time
+ * determining the original password.
+ *
+ * @param data the String to compute the hash of.
+ * @return a hashed version of the passed-in String
+ */
+ public synchronized static String hash(String data) {
+ if (digest == null) {
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ }
+ catch (NoSuchAlgorithmException nsae) {
+ System.err.println("Failed to load the SHA-1 MessageDigest. " +
+ "Jive will be unable to function normally.");
+ }
+ }
+ // Now, compute hash.
+ try {
+ digest.update(data.getBytes("UTF-8"));
+ }
+ catch (UnsupportedEncodingException e) {
+ System.err.println(e);
+ }
+ return encodeHex(digest.digest());
+ }
+
+ /**
+ * Encodes an array of bytes as String representation of hexadecimal.
+ *
+ * @param bytes an array of bytes to convert to a hex string.
+ * @return generated hex string.
+ */
+ public static String encodeHex(byte[] bytes) {
+ StringBuffer hex = new StringBuffer(bytes.length * 2);
+
+ for (int i=0; i<bytes.length; i++) {
+ if (((int) bytes[i] & 0xff) < 0x10) {
+ hex.append("0");
+ }
+ hex.append(Integer.toString((int) bytes[i] & 0xff, 16));
+ }
+
+ return hex.toString();
+ }
+
+ //*********************************************************************
+ //* Base64 - a simple base64 encoder and decoder.
+ //*
+ //* Copyright (c) 1999, Bob Withers - bwit@pobox.com
+ //*
+ //* This code may be freely used for any purpose, either personal
+ //* or commercial, provided the authors copyright notice remains
+ //* intact.
+ //*********************************************************************
+
+ private static final int fillchar = '=';
+ private static final String cvt = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ + "0123456789+/";
+
+ /**
+ * Encodes a String as a base64 String.
+ *
+ * @param data a String to encode.
+ * @return a base64 encoded String.
+ */
+ public static String encodeBase64(String data) {
+ byte [] bytes = null;
+ try {
+ bytes = data.getBytes("ISO-8859-1");
+ }
+ catch (UnsupportedEncodingException uee) {
+ uee.printStackTrace();
+ }
+ return encodeBase64(bytes);
+ }
+
+ /**
+ * Encodes a byte array into a base64 String.
+ *
+ * @param data a byte array to encode.
+ * @return a base64 encode String.
+ */
+ public static String encodeBase64(byte[] data) {
+ int c;
+ int len = data.length;
+ StringBuffer ret = new StringBuffer(((len / 3) + 1) * 4);
+ for (int i = 0; i < len; ++i) {
+ c = (data[i] >> 2) & 0x3f;
+ ret.append(cvt.charAt(c));
+ c = (data[i] << 4) & 0x3f;
+ if (++i < len)
+ c |= (data[i] >> 4) & 0x0f;
+
+ ret.append(cvt.charAt(c));
+ if (i < len) {
+ c = (data[i] << 2) & 0x3f;
+ if (++i < len)
+ c |= (data[i] >> 6) & 0x03;
+
+ ret.append(cvt.charAt(c));
+ }
+ else {
+ ++i;
+ ret.append((char) fillchar);
+ }
+
+ if (i < len) {
+ c = data[i] & 0x3f;
+ ret.append(cvt.charAt(c));
+ }
+ else {
+ ret.append((char) fillchar);
+ }
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Decodes a base64 String.
+ *
+ * @param data a base64 encoded String to decode.
+ * @return the decoded String.
+ */
+ public static byte[] decodeBase64(String data) {
+ byte [] bytes = null;
+ try {
+ bytes = data.getBytes("ISO-8859-1");
+ return decodeBase64(bytes).getBytes("ISO-8859-1");
+ }
+ catch (UnsupportedEncodingException uee) {
+ uee.printStackTrace();
+ }
+ return new byte[] { };
+ }
+
+ /**
+ * Decodes a base64 aray of bytes.
+ *
+ * @param data a base64 encode byte array to decode.
+ * @return the decoded String.
+ */
+ private static String decodeBase64(byte[] data) {
+ int c, c1;
+ int len = data.length;
+ StringBuffer ret = new StringBuffer((len * 3) / 4);
+ for (int i = 0; i < len; ++i) {
+ c = cvt.indexOf(data[i]);
+ ++i;
+ c1 = cvt.indexOf(data[i]);
+ c = ((c << 2) | ((c1 >> 4) & 0x3));
+ ret.append((char) c);
+ if (++i < len) {
+ c = data[i];
+ if (fillchar == c)
+ break;
+
+ c = cvt.indexOf(c);
+ c1 = ((c1 << 4) & 0xf0) | ((c >> 2) & 0xf);
+ ret.append((char) c1);
+ }
+
+ if (++i < len) {
+ c1 = data[i];
+ if (fillchar == c1)
+ break;
+
+ c1 = cvt.indexOf(c1);
+ c = ((c << 6) & 0xc0) | c1;
+ ret.append((char) c);
+ }
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Pseudo-random number generator object for use with randomString().
+ * The Random class is not considered to be cryptographically secure, so
+ * only use these random Strings for low to medium security applications.
+ */
+ private static Random randGen = new Random();
+
+ /**
+ * Array of numbers and letters of mixed case. Numbers appear in the list
+ * twice so that there is a more equal chance that a number will be picked.
+ * We can use the array to get a random number or letter by picking a random
+ * array index.
+ */
+ private static char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz" +
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray();
+
+ /**
+ * Returns a random String of numbers and letters (lower and upper case)
+ * of the specified length. The method uses the Random class that is
+ * built-in to Java which is suitable for low to medium grade security uses.
+ * This means that the output is only pseudo random, i.e., each number is
+ * mathematically generated so is not truly random.<p>
+ *
+ * The specified length must be at least one. If not, the method will return
+ * null.
+ *
+ * @param length the desired length of the random String to return.
+ * @return a random String of numbers and letters of the specified length.
+ */
+ public static final String randomString(int length) {
+ if (length < 1) {
+ return null;
+ }
+ // Create a char buffer to put random letters and numbers in.
+ char [] randBuffer = new char[length];
+ for (int i=0; i<randBuffer.length; i++) {
+ randBuffer[i] = numbersAndLetters[randGen.nextInt(71)];
+ }
+ return new String(randBuffer);
+ }
+
+ private StringUtils() {
+ // Not instantiable.
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/WriterListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/WriterListener.java
new file mode 100644
index 000000000..175f3a66d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/WriterListener.java
@@ -0,0 +1,41 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.util;
+
+/**
+ * Interface that allows for implementing classes to listen for string writing
+ * events. Listeners are registered with ObservableWriter objects.
+ *
+ * @see ObservableWriter#addWriterListener
+ * @see ObservableWriter#removeWriterListener
+ *
+ * @author Gaston Dombiak
+ */
+public interface WriterListener {
+
+ /**
+ * Notification that the Writer has written a new string.
+ *
+ * @param str the written string
+ */
+ public abstract void write(String str);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/package.html
new file mode 100644
index 000000000..e34bfe316
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/package.html
@@ -0,0 +1 @@
+<body>Utility classes.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java
new file mode 100644
index 000000000..5cfb15acf
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java
@@ -0,0 +1,55 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+/**
+ *
+ * Default implementation of the MessageEventRequestListener interface.<p>
+ *
+ * This class automatically sends a delivered notification to the sender of the message
+ * if the sender has requested to be notified when the message is delivered.
+ *
+ * @author Gaston Dombiak
+ */
+public class DefaultMessageEventRequestListener implements MessageEventRequestListener {
+
+ public void deliveredNotificationRequested(String from, String packetID,
+ MessageEventManager messageEventManager)
+ {
+ // Send to the message's sender that the message has been delivered
+ messageEventManager.sendDeliveredNotification(from, packetID);
+ }
+
+ public void displayedNotificationRequested(String from, String packetID,
+ MessageEventManager messageEventManager)
+ {
+ }
+
+ public void composingNotificationRequested(String from, String packetID,
+ MessageEventManager messageEventManager)
+ {
+ }
+
+ public void offlineNotificationRequested(String from, String packetID,
+ MessageEventManager messageEventManager)
+ {
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/Form.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/Form.java
new file mode 100644
index 000000000..0f9ae8084
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/Form.java
@@ -0,0 +1,539 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.*;
+
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smackx.packet.DataForm;
+
+/**
+ * Represents a Form for gathering data. The form could be of the following types:
+ * <ul>
+ * <li>form -> Indicates a form to fill out.</li>
+ * <li>submit -> The form is filled out, and this is the data that is being returned from
+ * the form.</li>
+ * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li>
+ * <li>result -> Data results being returned from a search, or some other query.</li>
+ * </ul>
+ *
+ * Depending of the form's type different operations are available. For example, it's only possible
+ * to set answers if the form is of type "submit".
+ *
+ * @author Gaston Dombiak
+ */
+public class Form {
+
+ public static final String TYPE_FORM = "form";
+ public static final String TYPE_SUBMIT = "submit";
+ public static final String TYPE_CANCEL = "cancel";
+ public static final String TYPE_RESULT = "result";
+
+ private DataForm dataForm;
+
+ /**
+ * Returns a new ReportedData if the packet is used for gathering data and includes an
+ * extension that matches the elementName and namespace "x","jabber:x:data".
+ *
+ * @param packet the packet used for gathering data.
+ */
+ public static Form getFormFrom(Packet packet) {
+ // Check if the packet includes the DataForm extension
+ PacketExtension packetExtension = packet.getExtension("x","jabber:x:data");
+ if (packetExtension != null) {
+ // Check if the existing DataForm is not a result of a search
+ DataForm dataForm = (DataForm) packetExtension;
+ if (dataForm.getReportedData() == null)
+ return new Form(dataForm);
+ }
+ // Otherwise return null
+ return null;
+ }
+
+ /**
+ * Creates a new Form that will wrap an existing DataForm. The wrapped DataForm must be
+ * used for gathering data.
+ *
+ * @param dataForm the data form used for gathering data.
+ */
+ private Form(DataForm dataForm) {
+ this.dataForm = dataForm;
+ }
+
+ /**
+ * Creates a new Form of a given type from scratch.<p>
+ *
+ * Possible form types are:
+ * <ul>
+ * <li>form -> Indicates a form to fill out.</li>
+ * <li>submit -> The form is filled out, and this is the data that is being returned from
+ * the form.</li>
+ * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li>
+ * <li>result -> Data results being returned from a search, or some other query.</li>
+ * </ul>
+ *
+ * @param type the form's type (e.g. form, submit,cancel,result).
+ */
+ public Form(String type) {
+ this.dataForm = new DataForm(type);
+ }
+
+ /**
+ * Adds a new field to complete as part of the form.
+ *
+ * @param field the field to complete.
+ */
+ public void addField(FormField field) {
+ dataForm.addField(field);
+ }
+
+ /**
+ * Sets a new String value to a given form's field. The field whose variable matches the
+ * requested variable will be completed with the specified value. If no field could be found
+ * for the specified variable then an exception will be raised.<p>
+ *
+ * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you
+ * can use this message where the String value is the String representation of the object.
+ *
+ * @param variable the variable name that was completed.
+ * @param value the String value that was answered.
+ * @throws IllegalStateException if the form is not of type "submit".
+ * @throws IllegalArgumentException if the form does not include the specified variable.
+ * @throws IllegalArgumentException if the answer type does not correspond with the field type.
+ */
+ public void setAnswer(String variable, String value) {
+ FormField field = getField(variable);
+ if (field == null) {
+ throw new IllegalArgumentException("Field not found for the specified variable name.");
+ }
+ if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+ && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+ && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())
+ && !FormField.TYPE_JID_SINGLE.equals(field.getType())
+ && !FormField.TYPE_HIDDEN.equals(field.getType())) {
+ throw new IllegalArgumentException("This field is not of type String.");
+ }
+ setAnswer(field, value);
+ }
+
+ /**
+ * Sets a new int value to a given form's field. The field whose variable matches the
+ * requested variable will be completed with the specified value. If no field could be found
+ * for the specified variable then an exception will be raised.
+ *
+ * @param variable the variable name that was completed.
+ * @param value the int value that was answered.
+ * @throws IllegalStateException if the form is not of type "submit".
+ * @throws IllegalArgumentException if the form does not include the specified variable.
+ * @throws IllegalArgumentException if the answer type does not correspond with the field type.
+ */
+ public void setAnswer(String variable, int value) {
+ FormField field = getField(variable);
+ if (field == null) {
+ throw new IllegalArgumentException("Field not found for the specified variable name.");
+ }
+ if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+ && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+ && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) {
+ throw new IllegalArgumentException("This field is not of type int.");
+ }
+ setAnswer(field, new Integer(value));
+ }
+
+ /**
+ * Sets a new long value to a given form's field. The field whose variable matches the
+ * requested variable will be completed with the specified value. If no field could be found
+ * for the specified variable then an exception will be raised.
+ *
+ * @param variable the variable name that was completed.
+ * @param value the long value that was answered.
+ * @throws IllegalStateException if the form is not of type "submit".
+ * @throws IllegalArgumentException if the form does not include the specified variable.
+ * @throws IllegalArgumentException if the answer type does not correspond with the field type.
+ */
+ public void setAnswer(String variable, long value) {
+ FormField field = getField(variable);
+ if (field == null) {
+ throw new IllegalArgumentException("Field not found for the specified variable name.");
+ }
+ if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+ && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+ && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) {
+ throw new IllegalArgumentException("This field is not of type long.");
+ }
+ setAnswer(field, new Long(value));
+ }
+
+ /**
+ * Sets a new float value to a given form's field. The field whose variable matches the
+ * requested variable will be completed with the specified value. If no field could be found
+ * for the specified variable then an exception will be raised.
+ *
+ * @param variable the variable name that was completed.
+ * @param value the float value that was answered.
+ * @throws IllegalStateException if the form is not of type "submit".
+ * @throws IllegalArgumentException if the form does not include the specified variable.
+ * @throws IllegalArgumentException if the answer type does not correspond with the field type.
+ */
+ public void setAnswer(String variable, float value) {
+ FormField field = getField(variable);
+ if (field == null) {
+ throw new IllegalArgumentException("Field not found for the specified variable name.");
+ }
+ if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+ && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+ && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) {
+ throw new IllegalArgumentException("This field is not of type float.");
+ }
+ setAnswer(field, new Float(value));
+ }
+
+ /**
+ * Sets a new double value to a given form's field. The field whose variable matches the
+ * requested variable will be completed with the specified value. If no field could be found
+ * for the specified variable then an exception will be raised.
+ *
+ * @param variable the variable name that was completed.
+ * @param value the double value that was answered.
+ * @throws IllegalStateException if the form is not of type "submit".
+ * @throws IllegalArgumentException if the form does not include the specified variable.
+ * @throws IllegalArgumentException if the answer type does not correspond with the field type.
+ */
+ public void setAnswer(String variable, double value) {
+ FormField field = getField(variable);
+ if (field == null) {
+ throw new IllegalArgumentException("Field not found for the specified variable name.");
+ }
+ if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+ && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+ && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) {
+ throw new IllegalArgumentException("This field is not of type double.");
+ }
+ setAnswer(field, new Double(value));
+ }
+
+ /**
+ * Sets a new boolean value to a given form's field. The field whose variable matches the
+ * requested variable will be completed with the specified value. If no field could be found
+ * for the specified variable then an exception will be raised.
+ *
+ * @param variable the variable name that was completed.
+ * @param value the boolean value that was answered.
+ * @throws IllegalStateException if the form is not of type "submit".
+ * @throws IllegalArgumentException if the form does not include the specified variable.
+ * @throws IllegalArgumentException if the answer type does not correspond with the field type.
+ */
+ public void setAnswer(String variable, boolean value) {
+ FormField field = getField(variable);
+ if (field == null) {
+ throw new IllegalArgumentException("Field not found for the specified variable name.");
+ }
+ if (!FormField.TYPE_BOOLEAN.equals(field.getType())) {
+ throw new IllegalArgumentException("This field is not of type boolean.");
+ }
+ setAnswer(field, (value ? "1" : "0"));
+ }
+
+ /**
+ * Sets a new Object value to a given form's field. In fact, the object representation
+ * (i.e. #toString) will be the actual value of the field.<p>
+ *
+ * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you
+ * will need to use {@link #setAnswer(String, String))} where the String value is the
+ * String representation of the object.<p>
+ *
+ * Before setting the new value to the field we will check if the form is of type submit. If
+ * the form isn't of type submit means that it's not possible to complete the form and an
+ * exception will be thrown.
+ *
+ * @param field the form field that was completed.
+ * @param value the Object value that was answered. The object representation will be the
+ * actual value.
+ * @throws IllegalStateException if the form is not of type "submit".
+ */
+ private void setAnswer(FormField field, Object value) {
+ if (!isSubmitType()) {
+ throw new IllegalStateException("Cannot set an answer if the form is not of type " +
+ "\"submit\"");
+ }
+ field.resetValues();
+ field.addValue(value.toString());
+ }
+
+ /**
+ * Sets a new values to a given form's field. The field whose variable matches the requested
+ * variable will be completed with the specified values. If no field could be found for
+ * the specified variable then an exception will be raised.<p>
+ *
+ * The Objects contained in the List could be of any type. The String representation of them
+ * (i.e. #toString) will be actually used when sending the answer to the server.
+ *
+ * @param variable the variable that was completed.
+ * @param values the values that were answered.
+ * @throws IllegalStateException if the form is not of type "submit".
+ * @throws IllegalArgumentException if the form does not include the specified variable.
+ */
+ public void setAnswer(String variable, List values) {
+ if (!isSubmitType()) {
+ throw new IllegalStateException("Cannot set an answer if the form is not of type " +
+ "\"submit\"");
+ }
+ FormField field = getField(variable);
+ if (field != null) {
+ // Check that the field can accept a collection of values
+ if (!FormField.TYPE_JID_MULTI.equals(field.getType())
+ && !FormField.TYPE_LIST_MULTI.equals(field.getType())
+ && !FormField.TYPE_LIST_SINGLE.equals(field.getType())
+ && !FormField.TYPE_HIDDEN.equals(field.getType())) {
+ throw new IllegalArgumentException("This field only accept list of values.");
+ }
+ // Clear the old values
+ field.resetValues();
+ // Set the new values. The string representation of each value will be actually used.
+ field.addValues(values);
+ }
+ else {
+ throw new IllegalArgumentException("Couldn't find a field for the specified variable.");
+ }
+ }
+
+ /**
+ * Sets the default value as the value of a given form's field. The field whose variable matches
+ * the requested variable will be completed with its default value. If no field could be found
+ * for the specified variable then an exception will be raised.
+ *
+ * @param variable the variable to complete with its default value.
+ * @throws IllegalStateException if the form is not of type "submit".
+ * @throws IllegalArgumentException if the form does not include the specified variable.
+ */
+ public void setDefaultAnswer(String variable) {
+ if (!isSubmitType()) {
+ throw new IllegalStateException("Cannot set an answer if the form is not of type " +
+ "\"submit\"");
+ }
+ FormField field = getField(variable);
+ if (field != null) {
+ // Clear the old values
+ field.resetValues();
+ // Set the default value
+ for (Iterator it = field.getValues(); it.hasNext();) {
+ field.addValue((String) it.next());
+ }
+ }
+ else {
+ throw new IllegalArgumentException("Couldn't find a field for the specified variable.");
+ }
+ }
+
+ /**
+ * Returns an Iterator for the fields that are part of the form.
+ *
+ * @return an Iterator for the fields that are part of the form.
+ */
+ public Iterator getFields() {
+ return dataForm.getFields();
+ }
+
+ /**
+ * Returns the field of the form whose variable matches the specified variable.
+ * The fields of type FIXED will never be returned since they do not specify a
+ * variable.
+ *
+ * @param variable the variable to look for in the form fields.
+ * @return the field of the form whose variable matches the specified variable.
+ */
+ public FormField getField(String variable) {
+ if (variable == null || variable.equals("")) {
+ throw new IllegalArgumentException("Variable must not be null or blank.");
+ }
+ // Look for the field whose variable matches the requested variable
+ FormField field;
+ for (Iterator it=getFields();it.hasNext();) {
+ field = (FormField)it.next();
+ if (variable.equals(field.getVariable())) {
+ return field;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the instructions that explain how to fill out the form and what the form is about.
+ *
+ * @return instructions that explain how to fill out the form.
+ */
+ public String getInstructions() {
+ StringBuffer sb = new StringBuffer();
+ // Join the list of instructions together separated by newlines
+ for (Iterator it = dataForm.getInstructions(); it.hasNext();) {
+ sb.append((String) it.next());
+ // If this is not the last instruction then append a newline
+ if (it.hasNext()) {
+ sb.append("\n");
+ }
+ }
+ return sb.toString();
+ }
+
+
+ /**
+ * Returns the description of the data. It is similar to the title on a web page or an X
+ * window. You can put a <title/> on either a form to fill out, or a set of data results.
+ *
+ * @return description of the data.
+ */
+ public String getTitle() {
+ return dataForm.getTitle();
+ }
+
+
+ /**
+ * Returns the meaning of the data within the context. The data could be part of a form
+ * to fill out, a form submission or data results.<p>
+ *
+ * Possible form types are:
+ * <ul>
+ * <li>form -> Indicates a form to fill out.</li>
+ * <li>submit -> The form is filled out, and this is the data that is being returned from
+ * the form.</li>
+ * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li>
+ * <li>result -> Data results being returned from a search, or some other query.</li>
+ * </ul>
+ *
+ * @return the form's type.
+ */
+ public String getType() {
+ return dataForm.getType();
+ }
+
+
+ /**
+ * Sets instructions that explain how to fill out the form and what the form is about.
+ *
+ * @param instructions instructions that explain how to fill out the form.
+ */
+ public void setInstructions(String instructions) {
+ // Split the instructions into multiple instructions for each existent newline
+ ArrayList instructionsList = new ArrayList();
+ StringTokenizer st = new StringTokenizer(instructions, "\n");
+ while (st.hasMoreTokens()) {
+ instructionsList.add(st.nextToken());
+ }
+ // Set the new list of instructions
+ dataForm.setInstructions(instructionsList);
+
+ }
+
+
+ /**
+ * Sets the description of the data. It is similar to the title on a web page or an X window.
+ * You can put a <title/> on either a form to fill out, or a set of data results.
+ *
+ * @param title description of the data.
+ */
+ public void setTitle(String title) {
+ dataForm.setTitle(title);
+ }
+
+ /**
+ * Returns a DataForm that serves to send this Form to the server. If the form is of type
+ * submit, it may contain fields with no value. These fields will be removed since they only
+ * exist to assist the user while editing/completing the form in a UI.
+ *
+ * @return the wrapped DataForm.
+ */
+ public DataForm getDataFormToSend() {
+ if (isSubmitType()) {
+ // Create a new DataForm that contains only the answered fields
+ DataForm dataFormToSend = new DataForm(getType());
+ for(Iterator it=getFields();it.hasNext();) {
+ FormField field = (FormField)it.next();
+ if (field.getValues().hasNext()) {
+ dataFormToSend.addField(field);
+ }
+ }
+ return dataFormToSend;
+ }
+ return dataForm;
+ }
+
+ /**
+ * Returns true if the form is a form to fill out.
+ *
+ * @return if the form is a form to fill out.
+ */
+ private boolean isFormType() {
+ return TYPE_FORM.equals(dataForm.getType());
+ }
+
+ /**
+ * Returns true if the form is a form to submit.
+ *
+ * @return if the form is a form to submit.
+ */
+ private boolean isSubmitType() {
+ return TYPE_SUBMIT.equals(dataForm.getType());
+ }
+
+ /**
+ * Returns a new Form to submit the completed values. The new Form will include all the fields
+ * of the original form except for the fields of type FIXED. Only the HIDDEN fields will
+ * include the same value of the original form. The other fields of the new form MUST be
+ * completed. If a field remains with no answer when sending the completed form, then it won't
+ * be included as part of the completed form.<p>
+ *
+ * The reason why the fields with variables are included in the new form is to provide a model
+ * for binding with any UI. This means that the UIs will use the original form (of type
+ * "form") to learn how to render the form, but the UIs will bind the fields to the form of
+ * type submit.
+ *
+ * @return a Form to submit the completed values.
+ */
+ public Form createAnswerForm() {
+ if (!isFormType()) {
+ throw new IllegalStateException("Only forms of type \"form\" could be answered");
+ }
+ // Create a new Form
+ Form form = new Form(TYPE_SUBMIT);
+ for (Iterator fields=getFields(); fields.hasNext();) {
+ FormField field = (FormField)fields.next();
+ // Add to the new form any type of field that includes a variable.
+ // Note: The fields of type FIXED are the only ones that don't specify a variable
+ if (field.getVariable() != null) {
+ FormField newField = new FormField(field.getVariable());
+ newField.setType(field.getType());
+ form.addField(newField);
+ // Set the answer ONLY to the hidden fields
+ if (FormField.TYPE_HIDDEN.equals(field.getType())) {
+ // Since a hidden field could have many values we need to collect them
+ // in a list
+ List values = new ArrayList();
+ for (Iterator it=field.getValues();it.hasNext();) {
+ values.add((String)it.next());
+ }
+ form.setAnswer(field.getVariable(), values);
+ }
+ }
+ }
+ return form;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/FormField.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/FormField.java
new file mode 100644
index 000000000..c8fafb965
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/FormField.java
@@ -0,0 +1,354 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.*;
+
+/**
+ * Represents a field of a form. The field could be used to represent a question to complete,
+ * a completed question or a data returned from a search. The exact interpretation of the field
+ * depends on the context where the field is used.
+ *
+ * @author Gaston Dombiak
+ */
+public class FormField {
+ public static final String TYPE_BOOLEAN = "boolean";
+ public static final String TYPE_FIXED = "fixed";
+ public static final String TYPE_HIDDEN = "hidden";
+ public static final String TYPE_JID_MULTI = "jid-multi";
+ public static final String TYPE_JID_SINGLE = "jid-single";
+ public static final String TYPE_LIST_MULTI = "list-multi";
+ public static final String TYPE_LIST_SINGLE = "list-single";
+ public static final String TYPE_TEXT_MULTI = "text-multi";
+ public static final String TYPE_TEXT_PRIVATE = "text-private";
+ public static final String TYPE_TEXT_SINGLE = "text-single";
+
+ private String description;
+ private boolean required = false;
+ private String label;
+ private String variable;
+ private String type;
+ private List options = new ArrayList();
+ private List values = new ArrayList();
+
+ /**
+ * Creates a new FormField with the variable name that uniquely identifies the field
+ * in the context of the form.
+ *
+ * @param variable the variable name of the question.
+ */
+ public FormField(String variable) {
+ this.variable = variable;
+ }
+
+ /**
+ * Creates a new FormField of type FIXED. The fields of type FIXED do not define a variable
+ * name.
+ *
+ */
+ public FormField() {
+ this.type = FormField.TYPE_FIXED;
+ }
+
+ /**
+ * Returns a description that provides extra clarification about the question. This information
+ * could be presented to the user either in tool-tip, help button, or as a section of text
+ * before the question.<p>
+ *
+ * If the question is of type FIXED then the description should remain empty.
+ *
+ * @return description that provides extra clarification about the question.
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Returns the label of the question which should give enough information to the user to
+ * fill out the form.
+ *
+ * @return label of the question.
+ */
+ public String getLabel() {
+ return label;
+ }
+
+ /**
+ * Returns an Iterator for the available options that the user has in order to answer
+ * the question.
+ *
+ * @return Iterator for the available options.
+ */
+ public Iterator getOptions() {
+ synchronized (options) {
+ return Collections.unmodifiableList(new ArrayList(options)).iterator();
+ }
+ }
+
+ /**
+ * Returns true if the question must be answered in order to complete the questionnaire.
+ *
+ * @return true if the question must be answered in order to complete the questionnaire.
+ */
+ public boolean isRequired() {
+ return required;
+ }
+
+ /**
+ * Returns an indicative of the format for the data to answer. Valid formats are:
+ *
+ * <ul>
+ * <li>text-single -> single line or word of text
+ * <li>text-private -> instead of showing the user what they typed, you show ***** to
+ * protect it
+ * <li>text-multi -> multiple lines of text entry
+ * <li>list-single -> given a list of choices, pick one
+ * <li>list-multi -> given a list of choices, pick one or more
+ * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0
+ * <li>fixed -> fixed for putting in text to show sections, or just advertise your web
+ * site in the middle of the form
+ * <li>hidden -> is not given to the user at all, but returned with the questionnaire
+ * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based
+ * on the rules for a JID.
+ * <li>jid-multi -> multiple entries for JIDs
+ * </ul>
+ *
+ * @return format for the data to answer.
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Returns an Iterator for the default values of the question if the question is part
+ * of a form to fill out. Otherwise, returns an Iterator for the answered values of
+ * the question.
+ *
+ * @return an Iterator for the default values or answered values of the question.
+ */
+ public Iterator getValues() {
+ synchronized (values) {
+ return Collections.unmodifiableList(new ArrayList(values)).iterator();
+ }
+ }
+
+ /**
+ * Returns the variable name that the question is filling out.
+ *
+ * @return the variable name of the question.
+ */
+ public String getVariable() {
+ return variable;
+ }
+
+ /**
+ * Sets a description that provides extra clarification about the question. This information
+ * could be presented to the user either in tool-tip, help button, or as a section of text
+ * before the question.<p>
+ *
+ * If the question is of type FIXED then the description should remain empty.
+ *
+ * @param description provides extra clarification about the question.
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * Sets the label of the question which should give enough information to the user to
+ * fill out the form.
+ *
+ * @param label the label of the question.
+ */
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ /**
+ * Sets if the question must be answered in order to complete the questionnaire.
+ *
+ * @param required if the question must be answered in order to complete the questionnaire.
+ */
+ public void setRequired(boolean required) {
+ this.required = required;
+ }
+
+ /**
+ * Sets an indicative of the format for the data to answer. Valid formats are:
+ *
+ * <ul>
+ * <li>text-single -> single line or word of text
+ * <li>text-private -> instead of showing the user what they typed, you show ***** to
+ * protect it
+ * <li>text-multi -> multiple lines of text entry
+ * <li>list-single -> given a list of choices, pick one
+ * <li>list-multi -> given a list of choices, pick one or more
+ * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0
+ * <li>fixed -> fixed for putting in text to show sections, or just advertise your web
+ * site in the middle of the form
+ * <li>hidden -> is not given to the user at all, but returned with the questionnaire
+ * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based
+ * on the rules for a JID.
+ * <li>jid-multi -> multiple entries for JIDs
+ * </ul>
+ *
+ * @param type an indicative of the format for the data to answer.
+ */
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ /**
+ * Adds a default value to the question if the question is part of a form to fill out.
+ * Otherwise, adds an answered value to the question.
+ *
+ * @param value a default value or an answered value of the question.
+ */
+ public void addValue(String value) {
+ synchronized (values) {
+ values.add(value);
+ }
+ }
+
+ /**
+ * Adds a default values to the question if the question is part of a form to fill out.
+ * Otherwise, adds an answered values to the question.
+ *
+ * @param newValues default values or an answered values of the question.
+ */
+ public void addValues(List newValues) {
+ synchronized (values) {
+ values.addAll(newValues);
+ }
+ }
+
+ /**
+ * Removes all the values of the field.
+ *
+ */
+ protected void resetValues() {
+ synchronized (values) {
+ values.removeAll(new ArrayList(values));
+ }
+ }
+
+ /**
+ * Adss an available options to the question that the user has in order to answer
+ * the question.
+ *
+ * @param option a new available option for the question.
+ */
+ public void addOption(Option option) {
+ synchronized (options) {
+ options.add(option);
+ }
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<field");
+ // Add attributes
+ if (getLabel() != null) {
+ buf.append(" label=\"").append(getLabel()).append("\"");
+ }
+ if (getVariable() != null) {
+ buf.append(" var=\"").append(getVariable()).append("\"");
+ }
+ if (getType() != null) {
+ buf.append(" type=\"").append(getType()).append("\"");
+ }
+ buf.append(">");
+ // Add elements
+ if (getDescription() != null) {
+ buf.append("<desc>").append(getDescription()).append("</desc>");
+ }
+ if (isRequired()) {
+ buf.append("<required/>");
+ }
+ // Loop through all the values and append them to the string buffer
+ for (Iterator i = getValues(); i.hasNext();) {
+ buf.append("<value>").append(i.next()).append("</value>");
+ }
+ // Loop through all the values and append them to the string buffer
+ for (Iterator i = getOptions(); i.hasNext();) {
+ buf.append(((Option)i.next()).toXML());
+ }
+ buf.append("</field>");
+ return buf.toString();
+ }
+
+ /**
+ *
+ * Represents the available option of a given FormField.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Option {
+ private String label;
+ private String value;
+
+ public Option(String value) {
+ this.value = value;
+ }
+
+ public Option(String label, String value) {
+ this.label = label;
+ this.value = value;
+ }
+
+ /**
+ * Returns the label that represents the option.
+ *
+ * @return the label that represents the option.
+ */
+ public String getLabel() {
+ return label;
+ }
+
+ /**
+ * Returns the value of the option.
+ *
+ * @return the value of the option.
+ */
+ public String getValue() {
+ return value;
+ }
+
+ public String toString(){
+ return getLabel();
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<option");
+ // Add attribute
+ if (getLabel() != null) {
+ buf.append(" label=\"").append(getLabel()).append("\"");
+ }
+ buf.append(">");
+ // Add element
+ buf.append("<value>").append(getValue()).append("</value>");
+
+ buf.append("</option>");
+ return buf.toString();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/GroupChatInvitation.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/GroupChatInvitation.java
new file mode 100644
index 000000000..c2ee81ad1
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/GroupChatInvitation.java
@@ -0,0 +1,115 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * A group chat invitation packet extension, which is used to invite other
+ * users to a group chat room. To invite a user to a group chat room, address
+ * a new message to the user and set the room name appropriately, as in the
+ * following code example:
+ *
+ * <pre>
+ * Message message = new Message("user@chat.example.com");
+ * message.setBody("Join me for a group chat!");
+ * message.addExtension(new GroupChatInvitation("room@chat.example.com"););
+ * con.sendPacket(message);
+ * </pre>
+ *
+ * To listen for group chat invitations, use a PacketExtensionFilter for the
+ * <tt>x</tt> element name and <tt>jabber:x:conference</tt> namespace, as in the
+ * following code example:
+ *
+ * <pre>
+ * PacketFilter filter = new PacketExtensionFilter("x", "jabber:x:conference");
+ * // Create a packet collector or packet listeners using the filter...
+ * </pre>
+ *
+ * <b>Note</b>: this protocol is outdated now that the Multi-User Chat (MUC) JEP is available
+ * (<a href="http://www.jabber.org/jeps/jep-0045.html">JEP-45</a>). However, most
+ * existing clients still use this older protocol. Once MUC support becomes more
+ * widespread, this API may be deprecated.
+ *
+ * @author Matt Tucker
+ */
+public class GroupChatInvitation implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "x";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "jabber:x:conference";
+
+ private String roomAddress;
+
+ /**
+ * Creates a new group chat invitation to the specified room address.
+ * GroupChat room addresses are in the form <tt>room@service</tt>,
+ * where <tt>service</tt> is the name of groupchat server, such as
+ * <tt>chat.example.com</tt>.
+ *
+ * @param roomAddress the address of the group chat room.
+ */
+ public GroupChatInvitation(String roomAddress) {
+ this.roomAddress = roomAddress;
+ }
+
+ /**
+ * Returns the address of the group chat room. GroupChat room addresses
+ * are in the form <tt>room@service</tt>, where <tt>service</tt> is
+ * the name of groupchat server, such as <tt>chat.example.com</tt>.
+ *
+ * @return the address of the group chat room.
+ */
+ public String getRoomAddress() {
+ return roomAddress;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<x xmlns=\"jabber:x:conference\" jid=\"").append(roomAddress).append("\"/>");
+ return buf.toString();
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+ public PacketExtension parseExtension (XmlPullParser parser) throws Exception {
+ String roomAddress = parser.getAttributeValue("", "jid");
+ // Advance to end of extension.
+ parser.next();
+ return new GroupChatInvitation(roomAddress);
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventManager.java
new file mode 100644
index 000000000..3fd6c6859
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventManager.java
@@ -0,0 +1,304 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smackx.packet.*;
+
+/**
+ * Manages message events requests and notifications. A MessageEventManager provides a high
+ * level access to request for notifications and send event notifications. It also provides
+ * an easy way to hook up custom logic when requests or notifications are received.
+ *
+ * @author Gaston Dombiak
+ */
+public class MessageEventManager {
+
+ private List messageEventNotificationListeners = new ArrayList();
+ private List messageEventRequestListeners = new ArrayList();
+
+ private XMPPConnection con;
+
+ private PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:event");
+ private PacketListener packetListener;
+
+ /**
+ * Creates a new message event manager.
+ *
+ * @param con an XMPPConnection.
+ */
+ public MessageEventManager(XMPPConnection con) {
+ this.con = con;
+ init();
+ }
+
+ /**
+ * Adds event notification requests to a message. For each event type that
+ * the user wishes event notifications from the message recepient for, <tt>true</tt>
+ * should be passed in to this method.
+ *
+ * @param message the message to add the requested notifications.
+ * @param offline specifies if the offline event is requested.
+ * @param delivered specifies if the delivered event is requested.
+ * @param displayed specifies if the displayed event is requested.
+ * @param composing specifies if the composing event is requested.
+ */
+ public static void addNotificationsRequests(Message message, boolean offline,
+ boolean delivered, boolean displayed, boolean composing)
+ {
+ // Create a MessageEvent Package and add it to the message
+ MessageEvent messageEvent = new MessageEvent();
+ messageEvent.setOffline(offline);
+ messageEvent.setDelivered(delivered);
+ messageEvent.setDisplayed(displayed);
+ messageEvent.setComposing(composing);
+ message.addExtension(messageEvent);
+ }
+
+ /**
+ * Adds a message event request listener. The listener will be fired anytime a request for
+ * event notification is received.
+ *
+ * @param messageEventRequestListener a message event request listener.
+ */
+ public void addMessageEventRequestListener(MessageEventRequestListener messageEventRequestListener) {
+ synchronized (messageEventRequestListeners) {
+ if (!messageEventRequestListeners.contains(messageEventRequestListener)) {
+ messageEventRequestListeners.add(messageEventRequestListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a message event request listener. The listener will be fired anytime a request for
+ * event notification is received.
+ *
+ * @param messageEventRequestListener a message event request listener.
+ */
+ public void removeMessageEventRequestListener(MessageEventRequestListener messageEventRequestListener) {
+ synchronized (messageEventRequestListeners) {
+ messageEventRequestListeners.remove(messageEventRequestListener);
+ }
+ }
+
+ /**
+ * Adds a message event notification listener. The listener will be fired anytime a notification
+ * event is received.
+ *
+ * @param messageEventNotificationListener a message event notification listener.
+ */
+ public void addMessageEventNotificationListener(MessageEventNotificationListener messageEventNotificationListener) {
+ synchronized (messageEventNotificationListeners) {
+ if (!messageEventNotificationListeners.contains(messageEventNotificationListener)) {
+ messageEventNotificationListeners.add(messageEventNotificationListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a message event notification listener. The listener will be fired anytime a notification
+ * event is received.
+ *
+ * @param messageEventNotificationListener a message event notification listener.
+ */
+ public void removeMessageEventNotificationListener(MessageEventNotificationListener messageEventNotificationListener) {
+ synchronized (messageEventNotificationListeners) {
+ messageEventNotificationListeners.remove(messageEventNotificationListener);
+ }
+ }
+
+ /**
+ * Fires message event request listeners.
+ */
+ private void fireMessageEventRequestListeners(
+ String from,
+ String packetID,
+ String methodName) {
+ MessageEventRequestListener[] listeners = null;
+ Method method;
+ synchronized (messageEventRequestListeners) {
+ listeners = new MessageEventRequestListener[messageEventRequestListeners.size()];
+ messageEventRequestListeners.toArray(listeners);
+ }
+ try {
+ method =
+ MessageEventRequestListener.class.getDeclaredMethod(
+ methodName,
+ new Class[] { String.class, String.class, MessageEventManager.class });
+ for (int i = 0; i < listeners.length; i++) {
+ method.invoke(listeners[i], new Object[] { from, packetID, this });
+ }
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Fires message event notification listeners.
+ */
+ private void fireMessageEventNotificationListeners(
+ String from,
+ String packetID,
+ String methodName) {
+ MessageEventNotificationListener[] listeners = null;
+ Method method;
+ synchronized (messageEventNotificationListeners) {
+ listeners =
+ new MessageEventNotificationListener[messageEventNotificationListeners.size()];
+ messageEventNotificationListeners.toArray(listeners);
+ }
+ try {
+ method =
+ MessageEventNotificationListener.class.getDeclaredMethod(
+ methodName,
+ new Class[] { String.class, String.class });
+ for (int i = 0; i < listeners.length; i++) {
+ method.invoke(listeners[i], new Object[] { from, packetID });
+ }
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void init() {
+ // Listens for all message event packets and fire the proper message event listeners.
+ packetListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ Message message = (Message) packet;
+ MessageEvent messageEvent =
+ (MessageEvent) message.getExtension("x", "jabber:x:event");
+ if (messageEvent.isMessageEventRequest()) {
+ // Fire event for requests of message events
+ for (Iterator it = messageEvent.getEventTypes(); it.hasNext();)
+ fireMessageEventRequestListeners(
+ message.getFrom(),
+ message.getPacketID(),
+ ((String) it.next()).concat("NotificationRequested"));
+ } else
+ // Fire event for notifications of message events
+ for (Iterator it = messageEvent.getEventTypes(); it.hasNext();)
+ fireMessageEventNotificationListeners(
+ message.getFrom(),
+ messageEvent.getPacketID(),
+ ((String) it.next()).concat("Notification"));
+
+ };
+
+ };
+ con.addPacketListener(packetListener, packetFilter);
+ }
+
+ /**
+ * Sends the notification that the message was delivered to the sender of the original message
+ *
+ * @param to the recipient of the notification.
+ * @param packetID the id of the message to send.
+ */
+ public void sendDeliveredNotification(String to, String packetID) {
+ // Create the message to send
+ Message msg = new Message(to);
+ // Create a MessageEvent Package and add it to the message
+ MessageEvent messageEvent = new MessageEvent();
+ messageEvent.setDelivered(true);
+ messageEvent.setPacketID(packetID);
+ msg.addExtension(messageEvent);
+ // Send the packet
+ con.sendPacket(msg);
+ }
+
+ /**
+ * Sends the notification that the message was displayed to the sender of the original message
+ *
+ * @param to the recipient of the notification.
+ * @param packetID the id of the message to send.
+ */
+ public void sendDisplayedNotification(String to, String packetID) {
+ // Create the message to send
+ Message msg = new Message(to);
+ // Create a MessageEvent Package and add it to the message
+ MessageEvent messageEvent = new MessageEvent();
+ messageEvent.setDisplayed(true);
+ messageEvent.setPacketID(packetID);
+ msg.addExtension(messageEvent);
+ // Send the packet
+ con.sendPacket(msg);
+ }
+
+ /**
+ * Sends the notification that the receiver of the message is composing a reply
+ *
+ * @param to the recipient of the notification.
+ * @param packetID the id of the message to send.
+ */
+ public void sendComposingNotification(String to, String packetID) {
+ // Create the message to send
+ Message msg = new Message(to);
+ // Create a MessageEvent Package and add it to the message
+ MessageEvent messageEvent = new MessageEvent();
+ messageEvent.setComposing(true);
+ messageEvent.setPacketID(packetID);
+ msg.addExtension(messageEvent);
+ // Send the packet
+ con.sendPacket(msg);
+ }
+
+ /**
+ * Sends the notification that the receiver of the message has cancelled composing a reply.
+ *
+ * @param to the recipient of the notification.
+ * @param packetID the id of the message to send.
+ */
+ public void sendCancelledNotification(String to, String packetID) {
+ // Create the message to send
+ Message msg = new Message(to);
+ // Create a MessageEvent Package and add it to the message
+ MessageEvent messageEvent = new MessageEvent();
+ messageEvent.setCancelled(true);
+ messageEvent.setPacketID(packetID);
+ msg.addExtension(messageEvent);
+ // Send the packet
+ con.sendPacket(msg);
+ }
+
+ public void destroy() {
+ if (con != null) {
+ con.removePacketListener(packetListener);
+ }
+ }
+
+ public void finalize() {
+ destroy();
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventNotificationListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventNotificationListener.java
new file mode 100644
index 000000000..e6442af40
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventNotificationListener.java
@@ -0,0 +1,74 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+/**
+ *
+ * A listener that is fired anytime a message event notification is received.
+ * Message event notifications are received as a consequence of the request
+ * to receive notifications when sending a message.
+ *
+ * @author Gaston Dombiak
+ */
+public interface MessageEventNotificationListener {
+
+ /**
+ * Called when a notification of message delivered is received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ */
+ public void deliveredNotification(String from, String packetID);
+
+ /**
+ * Called when a notification of message displayed is received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ */
+ public void displayedNotification(String from, String packetID);
+
+ /**
+ * Called when a notification that the receiver of the message is composing a reply is
+ * received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ */
+ public void composingNotification(String from, String packetID);
+
+ /**
+ * Called when a notification that the receiver of the message is offline is received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ */
+ public void offlineNotification(String from, String packetID);
+
+ /**
+ * Called when a notification that the receiver of the message cancelled the reply
+ * is received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ */
+ public void cancelledNotification(String from, String packetID);
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventRequestListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventRequestListener.java
new file mode 100644
index 000000000..107168ff4
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventRequestListener.java
@@ -0,0 +1,86 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+/**
+ *
+ * A listener that is fired anytime a message event request is received.
+ * Message event requests are received when the received message includes an extension
+ * like this:
+ *
+ * <pre>
+ * &lt;x xmlns='jabber:x:event'&gt;
+ * &lt;offline/&gt;
+ * &lt;delivered/&gt;
+ * &lt;composing/&gt;
+ * &lt;/x&gt;
+ * </pre>
+ *
+ * In this example you can see that the sender of the message requests to be notified
+ * when the user couldn't receive the message because he/she is offline, the message
+ * was delivered or when the receiver of the message is composing a reply.
+ *
+ * @author Gaston Dombiak
+ */
+public interface MessageEventRequestListener {
+
+ /**
+ * Called when a request for message delivered notification is received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ * @param messageEventManager the messageEventManager that fired the listener.
+ */
+ public void deliveredNotificationRequested(String from, String packetID,
+ MessageEventManager messageEventManager);
+
+ /**
+ * Called when a request for message displayed notification is received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ * @param messageEventManager the messageEventManager that fired the listener.
+ */
+ public void displayedNotificationRequested(String from, String packetID,
+ MessageEventManager messageEventManager);
+
+ /**
+ * Called when a request that the receiver of the message is composing a reply notification is
+ * received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ * @param messageEventManager the messageEventManager that fired the listener.
+ */
+ public void composingNotificationRequested(String from, String packetID,
+ MessageEventManager messageEventManager);
+
+ /**
+ * Called when a request that the receiver of the message is offline is received.
+ *
+ * @param from the user that sent the notification.
+ * @param packetID the id of the message that was sent.
+ * @param messageEventManager the messageEventManager that fired the listener.
+ */
+ public void offlineNotificationRequested(String from, String packetID,
+ MessageEventManager messageEventManager);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientInfo.java
new file mode 100644
index 000000000..c6aa004fd
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientInfo.java
@@ -0,0 +1,98 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import org.jivesoftware.smackx.packet.MultipleAddresses;
+
+import java.util.List;
+
+/**
+ * MultipleRecipientInfo keeps information about the multiple recipients extension included
+ * in a received packet. Among the information we can find the list of TO and CC addresses.
+ *
+ * @author Gaston Dombiak
+ */
+public class MultipleRecipientInfo {
+
+ MultipleAddresses extension;
+
+ MultipleRecipientInfo(MultipleAddresses extension) {
+ this.extension = extension;
+ }
+
+ /**
+ * Returns the list of {@link org.jivesoftware.smackx.packet.MultipleAddresses.Address}
+ * that were the primary recipients of the packet.
+ *
+ * @return list of primary recipients of the packet.
+ */
+ public List getTOAddresses() {
+ return extension.getAddressesOfType(MultipleAddresses.TO);
+ }
+
+ /**
+ * Returns the list of {@link org.jivesoftware.smackx.packet.MultipleAddresses.Address}
+ * that were the secondary recipients of the packet.
+ *
+ * @return list of secondary recipients of the packet.
+ */
+ public List getCCAddresses() {
+ return extension.getAddressesOfType(MultipleAddresses.CC);
+ }
+
+ /**
+ * Returns the JID of a MUC room to which responses should be sent or <tt>null</tt> if
+ * no specific address was provided. When no specific address was provided then the reply
+ * can be sent to any or all recipients. Otherwise, the user should join the specified room
+ * and send the reply to the room.
+ *
+ * @return the JID of a MUC room to which responses should be sent or <tt>null</tt> if
+ * no specific address was provided.
+ */
+ public String getReplyRoom() {
+ List replyRoom = extension.getAddressesOfType(MultipleAddresses.REPLY_ROOM);
+ return replyRoom.isEmpty() ? null : ((MultipleAddresses.Address) replyRoom.get(0)).getJid();
+ }
+
+ /**
+ * Returns true if the received packet should not be replied. Use
+ * {@link MultipleRecipientManager#reply(org.jivesoftware.smack.XMPPConnection, org.jivesoftware.smack.packet.Message, org.jivesoftware.smack.packet.Message)}
+ * to send replies.
+ *
+ * @return true if the received packet should not be replied.
+ */
+ public boolean shouldNotReply() {
+ return !extension.getAddressesOfType(MultipleAddresses.NO_REPLY).isEmpty();
+ }
+
+ /**
+ * Returns the address to which all replies are requested to be sent or <tt>null</tt> if
+ * no specific address was provided. When no specific address was provided then the reply
+ * can be sent to any or all recipients.
+ *
+ * @return the address to which all replies are requested to be sent or <tt>null</tt> if
+ * no specific address was provided.
+ */
+ public MultipleAddresses.Address getReplyAddress() {
+ List replyTo = extension.getAddressesOfType(MultipleAddresses.REPLY_TO);
+ return replyTo.isEmpty() ? null : (MultipleAddresses.Address) replyTo.get(0);
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientManager.java
new file mode 100644
index 000000000..853836812
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientManager.java
@@ -0,0 +1,353 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.Cache;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.MultipleAddresses;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A MultipleRecipientManager allows to send packets to multiple recipients by making use of
+ * <a href="http://www.jabber.org/jeps/jep-0033.html">JEP-33: Extended Stanza Addressing</a>.
+ * It also allows to send replies to packets that were sent to multiple recipients.
+ *
+ * @author Gaston Dombiak
+ */
+public class MultipleRecipientManager {
+
+ /**
+ * Create a cache to hold the 100 most recently accessed elements for a period of
+ * 24 hours.
+ */
+ private static Cache services = new Cache(100, 24 * 60 * 60 * 1000);
+
+ /**
+ * Sends the specified packet to the list of specified recipients using the
+ * specified connection. If the server has support for JEP-33 then only one
+ * packet is going to be sent to the server with the multiple recipient instructions.
+ * However, if JEP-33 is not supported by the server then the client is going to send
+ * the packet to each recipient.
+ *
+ * @param connection the connection to use to send the packet.
+ * @param packet the packet to send to the list of recipients.
+ * @param to the list of JIDs to include in the TO list or <tt>null</tt> if no TO
+ * list exists.
+ * @param cc the list of JIDs to include in the CC list or <tt>null</tt> if no CC
+ * list exists.
+ * @param bcc the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
+ * list exists.
+ * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and
+ * some JEP-33 specific features were requested.
+ */
+ public static void send(XMPPConnection connection, Packet packet, List to, List cc, List bcc)
+ throws XMPPException {
+ send(connection, packet, to, cc, bcc, null, null, false);
+ }
+
+ /**
+ * Sends the specified packet to the list of specified recipients using the
+ * specified connection. If the server has support for JEP-33 then only one
+ * packet is going to be sent to the server with the multiple recipient instructions.
+ * However, if JEP-33 is not supported by the server then the client is going to send
+ * the packet to each recipient.
+ *
+ * @param connection the connection to use to send the packet.
+ * @param packet the packet to send to the list of recipients.
+ * @param to the list of JIDs to include in the TO list or <tt>null</tt> if no TO
+ * list exists.
+ * @param cc the list of JIDs to include in the CC list or <tt>null</tt> if no CC
+ * list exists.
+ * @param bcc the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
+ * list exists.
+ * @param replyTo address to which all replies are requested to be sent or <tt>null</tt>
+ * indicating that they can reply to any address.
+ * @param replyRoom JID of a MUC room to which responses should be sent or <tt>null</tt>
+ * indicating that they can reply to any address.
+ * @param noReply true means that receivers should not reply to the message.
+ * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and
+ * some JEP-33 specific features were requested.
+ */
+ public static void send(XMPPConnection connection, Packet packet, List to, List cc, List bcc,
+ String replyTo, String replyRoom, boolean noReply) throws XMPPException {
+ String serviceAddress = getMultipleRecipienServiceAddress(connection);
+ if (serviceAddress != null) {
+ // Send packet to target users using multiple recipient service provided by the server
+ sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply,
+ serviceAddress);
+ }
+ else {
+ // Server does not support JEP-33 so try to send the packet to each recipient
+ if (noReply || (replyTo != null && replyTo.trim().length() > 0) ||
+ (replyRoom != null && replyRoom.trim().length() > 0)) {
+ // Some specified JEP-33 features were requested so throw an exception alerting
+ // the user that this features are not available
+ throw new XMPPException("Extended Stanza Addressing not supported by server");
+ }
+ // Send the packet to each individual recipient
+ sendToIndividualRecipients(connection, packet, to, cc, bcc);
+ }
+ }
+
+ /**
+ * Sends a reply to a previously received packet that was sent to multiple recipients. Before
+ * attempting to send the reply message some checkings are performed. If any of those checkings
+ * fail then an XMPPException is going to be thrown with the specific error detail.
+ *
+ * @param connection the connection to use to send the reply.
+ * @param original the previously received packet that was sent to multiple recipients.
+ * @param reply the new message to send as a reply.
+ * @throws XMPPException if the original message was not sent to multiple recipients, or the
+ * original message cannot be replied or reply should be sent to a room.
+ */
+ public static void reply(XMPPConnection connection, Message original, Message reply)
+ throws XMPPException {
+ MultipleRecipientInfo info = getMultipleRecipientInfo(original);
+ if (info == null) {
+ throw new XMPPException("Original message does not contain multiple recipient info");
+ }
+ if (info.shouldNotReply()) {
+ throw new XMPPException("Original message should not be replied");
+ }
+ if (info.getReplyRoom() != null) {
+ throw new XMPPException("Reply should be sent through a room");
+ }
+ // Any <thread/> element from the initial message MUST be copied into the reply.
+ if (original.getThread() != null) {
+ reply.setThread(original.getThread());
+ }
+ MultipleAddresses.Address replyAddress = info.getReplyAddress();
+ if (replyAddress != null && replyAddress.getJid() != null) {
+ // Send reply to the reply_to address
+ reply.setTo(replyAddress.getJid());
+ connection.sendPacket(reply);
+ }
+ else {
+ // Send reply to multiple recipients
+ List to = new ArrayList();
+ List cc = new ArrayList();
+ for (Iterator it = info.getTOAddresses().iterator(); it.hasNext();) {
+ String jid = ((MultipleAddresses.Address) it.next()).getJid();
+ to.add(jid);
+ }
+ for (Iterator it = info.getCCAddresses().iterator(); it.hasNext();) {
+ String jid = ((MultipleAddresses.Address) it.next()).getJid();
+ cc.add(jid);
+ }
+ // Add original sender as a 'to' address (if not already present)
+ if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) {
+ to.add(original.getFrom());
+ }
+ // Remove the sender from the TO/CC list (try with bare JID too)
+ String from = connection.getUser();
+ if (!to.remove(from) && !cc.remove(from)) {
+ String bareJID = StringUtils.parseBareAddress(from);
+ to.remove(bareJID);
+ cc.remove(bareJID);
+ }
+
+ String serviceAddress = getMultipleRecipienServiceAddress(connection);
+ if (serviceAddress != null) {
+ // Send packet to target users using multiple recipient service provided by the server
+ sendThroughService(connection, reply, to, cc, null, null, null, false,
+ serviceAddress);
+ }
+ else {
+ // Server does not support JEP-33 so try to send the packet to each recipient
+ sendToIndividualRecipients(connection, reply, to, cc, null);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link MultipleRecipientInfo} contained in the specified packet or
+ * <tt>null</tt> if none was found. Only packets sent to multiple recipients will
+ * contain such information.
+ *
+ * @param packet the packet to check.
+ * @return the MultipleRecipientInfo contained in the specified packet or <tt>null</tt>
+ * if none was found.
+ */
+ public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) {
+ MultipleAddresses extension = (MultipleAddresses) packet
+ .getExtension("addresses", "http://jabber.org/protocol/address");
+ return extension == null ? null : new MultipleRecipientInfo(extension);
+ }
+
+ private static void sendToIndividualRecipients(XMPPConnection connection, Packet packet,
+ List to, List cc, List bcc) {
+ if (to != null) {
+ for (Iterator it = to.iterator(); it.hasNext();) {
+ String jid = (String) it.next();
+ packet.setTo(jid);
+ connection.sendPacket(new PacketCopy(packet.toXML()));
+ }
+ }
+ if (cc != null) {
+ for (Iterator it = cc.iterator(); it.hasNext();) {
+ String jid = (String) it.next();
+ packet.setTo(jid);
+ connection.sendPacket(new PacketCopy(packet.toXML()));
+ }
+ }
+ if (bcc != null) {
+ for (Iterator it = bcc.iterator(); it.hasNext();) {
+ String jid = (String) it.next();
+ packet.setTo(jid);
+ connection.sendPacket(new PacketCopy(packet.toXML()));
+ }
+ }
+ }
+
+ private static void sendThroughService(XMPPConnection connection, Packet packet, List to,
+ List cc, List bcc, String replyTo, String replyRoom, boolean noReply,
+ String serviceAddress) {
+ // Create multiple recipient extension
+ MultipleAddresses multipleAddresses = new MultipleAddresses();
+ if (to != null) {
+ for (Iterator it = to.iterator(); it.hasNext();) {
+ String jid = (String) it.next();
+ multipleAddresses.addAddress(MultipleAddresses.TO, jid, null, null, false, null);
+ }
+ }
+ if (cc != null) {
+ for (Iterator it = cc.iterator(); it.hasNext();) {
+ String jid = (String) it.next();
+ multipleAddresses.addAddress(MultipleAddresses.CC, jid, null, null, false, null);
+ }
+ }
+ if (bcc != null) {
+ for (Iterator it = bcc.iterator(); it.hasNext();) {
+ String jid = (String) it.next();
+ multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null, null, false, null);
+ }
+ }
+ if (noReply) {
+ multipleAddresses.setNoReply();
+ }
+ else {
+ if (replyTo != null && replyTo.trim().length() > 0) {
+ multipleAddresses
+ .addAddress(MultipleAddresses.REPLY_TO, replyTo, null, null, false, null);
+ }
+ if (replyRoom != null && replyRoom.trim().length() > 0) {
+ multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM, replyRoom, null, null,
+ false, null);
+ }
+ }
+ // Set the multiple recipient service address as the target address
+ packet.setTo(serviceAddress);
+ // Add extension to packet
+ packet.addExtension(multipleAddresses);
+ // Send the packet
+ connection.sendPacket(packet);
+ }
+
+ /**
+ * Returns the address of the multiple recipients service. To obtain such address service
+ * discovery is going to be used on the connected server and if none was found then another
+ * attempt will be tried on the server items. The discovered information is going to be
+ * cached for 24 hours.
+ *
+ * @param connection the connection to use for disco. The connected server is going to be
+ * queried.
+ * @return the address of the multiple recipients service or <tt>null</tt> if none was found.
+ */
+ private static String getMultipleRecipienServiceAddress(XMPPConnection connection) {
+ String serviceName = connection.getServiceName();
+ String serviceAddress = (String) services.get(serviceName);
+ if (serviceAddress == null) {
+ synchronized (services) {
+ serviceAddress = (String) services.get(serviceName);
+ if (serviceAddress == null) {
+
+ // Send the disco packet to the server itself
+ try {
+ DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection)
+ .discoverInfo(serviceName);
+ // Check if the server supports JEP-33
+ if (info.containsFeature("http://jabber.org/protocol/address")) {
+ serviceAddress = serviceName;
+ }
+ else {
+ // Get the disco items and send the disco packet to each server item
+ DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection)
+ .discoverItems(serviceName);
+ for (Iterator it = items.getItems(); it.hasNext();) {
+ DiscoverItems.Item item = (DiscoverItems.Item) it.next();
+ info = ServiceDiscoveryManager.getInstanceFor(connection)
+ .discoverInfo(item.getEntityID(), item.getNode());
+ if (info.containsFeature("http://jabber.org/protocol/address")) {
+ serviceAddress = serviceName;
+ break;
+ }
+ }
+
+ }
+ // Cache the discovered information
+ services.put(serviceName, serviceAddress == null ? "" : serviceAddress);
+ }
+ catch (XMPPException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ return "".equals(serviceAddress) ? null : serviceAddress;
+ }
+
+ /**
+ * Packet that holds the XML stanza to send. This class is useful when the same packet
+ * is needed to be sent to different recipients. Since using the same packet is not possible
+ * (i.e. cannot change the TO address of a queues packet to be sent) then this class was
+ * created to keep the XML stanza to send.
+ */
+ private static class PacketCopy extends Packet {
+
+ private String text;
+
+ /**
+ * Create a copy of a packet with the text to send. The passed text must be a valid text to
+ * send to the server, no validation will be done on the passed text.
+ *
+ * @param text the whole text of the packet to send
+ */
+ public PacketCopy(String text) {
+ this.text = text;
+ }
+
+ public String toXML() {
+ return text;
+ }
+
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/NodeInformationProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/NodeInformationProvider.java
new file mode 100644
index 000000000..2a06e2f8d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/NodeInformationProvider.java
@@ -0,0 +1,44 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.Iterator;
+
+
+/**
+ * The NodeInformationProvider is responsible for providing information (i.e. DiscoverItems.Item)
+ * about a given node. This information will be requested each time this XMPPP client receives a
+ * disco items requests on the given node.
+ *
+ * @author Gaston Dombiak
+ */
+public interface NodeInformationProvider {
+
+ /**
+ * Returns an Iterator on the Items {@link org.jivesoftware.smackx.packet.DiscoverItems.Item}
+ * defined in the node. For example, the MUC protocol specifies that an XMPP client should
+ * answer an Item for each joined room when asked for the rooms where the use has joined.
+ *
+ * @return an Iterator on the Items defined in the node.
+ */
+ public abstract Iterator getNodeItems();
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageHeader.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageHeader.java
new file mode 100644
index 000000000..c3ff215a1
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageHeader.java
@@ -0,0 +1,85 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import org.jivesoftware.smackx.packet.DiscoverItems;
+
+/**
+ * The OfflineMessageHeader holds header information of an offline message. The header
+ * information was retrieved using the {@link OfflineMessageManager} class.<p>
+ *
+ * Each offline message is identified by the target user of the offline message and a unique stamp.
+ * Use {@link OfflineMessageManager#getMessages(java.util.List)} to retrieve the whole message.
+ *
+ * @author Gaston Dombiak
+ */
+public class OfflineMessageHeader {
+ /**
+ * Bare JID of the user that was offline when the message was sent.
+ */
+ private String user;
+ /**
+ * Full JID of the user that sent the message.
+ */
+ private String jid;
+ /**
+ * Stamp that uniquely identifies the offline message. This stamp will be used for
+ * getting the specific message or delete it. The stamp may be of the form UTC timestamps
+ * but it is not required to have that format.
+ */
+ private String stamp;
+
+ public OfflineMessageHeader(DiscoverItems.Item item) {
+ super();
+ user = item.getEntityID();
+ jid = item.getName();
+ stamp = item.getNode();
+ }
+
+ /**
+ * Returns the bare JID of the user that was offline when the message was sent.
+ *
+ * @return the bare JID of the user that was offline when the message was sent.
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * Returns the full JID of the user that sent the message.
+ *
+ * @return the full JID of the user that sent the message.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the stamp that uniquely identifies the offline message. This stamp will
+ * be used for getting the specific message or delete it. The stamp may be of the
+ * form UTC timestamps but it is not required to have that format.
+ *
+ * @return the stamp that uniquely identifies the offline message.
+ */
+ public String getStamp() {
+ return stamp;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageManager.java
new file mode 100644
index 000000000..ba4331d83
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageManager.java
@@ -0,0 +1,284 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.OfflineMessageInfo;
+import org.jivesoftware.smackx.packet.OfflineMessageRequest;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The OfflineMessageManager helps manage offline messages even before the user has sent an
+ * available presence. When a user asks for his offline messages before sending an available
+ * presence then the server will not send a flood with all the offline messages when the user
+ * becomes online. The server will not send a flood with all the offline messages to the session
+ * that made the offline messages request or to any other session used by the user that becomes
+ * online.<p>
+ *
+ * Once the session that made the offline messages request has been closed and the user becomes
+ * offline in all the resources then the server will resume storing the messages offline and will
+ * send all the offline messages to the user when he becomes online. Therefore, the server will
+ * flood the user when he becomes online unless the user uses this class to manage his offline
+ * messages.
+ *
+ * @author Gaston Dombiak
+ */
+public class OfflineMessageManager {
+
+ private final static String namespace = "http://jabber.org/protocol/offline";
+
+ private XMPPConnection connection;
+
+ private PacketFilter packetFilter;
+
+ public OfflineMessageManager(XMPPConnection connection) {
+ this.connection = connection;
+ packetFilter =
+ new AndFilter(new PacketExtensionFilter("offline", namespace),
+ new PacketTypeFilter(Message.class));
+ }
+
+ /**
+ * Returns true if the server supports Flexible Offline Message Retrieval. When the server
+ * supports Flexible Offline Message Retrieval it is possible to get the header of the offline
+ * messages, get specific messages, delete specific messages, etc.
+ *
+ * @return a boolean indicating if the server supports Flexible Offline Message Retrieval.
+ * @throws XMPPException If the user is not allowed to make this request.
+ */
+ public boolean supportsFlexibleRetrieval() throws XMPPException {
+ DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(null);
+ return info.containsFeature(namespace);
+ }
+
+ /**
+ * Returns the number of offline messages for the user of the connection.
+ *
+ * @return the number of offline messages for the user of the connection.
+ * @throws XMPPException If the user is not allowed to make this request or the server does
+ * not support offline message retrieval.
+ */
+ public int getMessageCount() throws XMPPException {
+ DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(null,
+ namespace);
+ Form extendedInfo = Form.getFormFrom(info);
+ if (extendedInfo != null) {
+ String value = (String) extendedInfo.getField("number_of_messages").getValues().next();
+ return Integer.parseInt(value);
+ }
+ return 0;
+ }
+
+ /**
+ * Returns an iterator on <tt>OfflineMessageHeader</tt> that keep information about the
+ * offline message. The OfflineMessageHeader includes a stamp that could be used to retrieve
+ * the complete message or delete the specific message.
+ *
+ * @return an iterator on <tt>OfflineMessageHeader</tt> that keep information about the offline
+ * message.
+ * @throws XMPPException If the user is not allowed to make this request or the server does
+ * not support offline message retrieval.
+ */
+ public Iterator getHeaders() throws XMPPException {
+ List answer = new ArrayList();
+ DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(
+ null, namespace);
+ for (Iterator it = items.getItems(); it.hasNext();) {
+ DiscoverItems.Item item = (DiscoverItems.Item) it.next();
+ answer.add(new OfflineMessageHeader(item));
+ }
+ return answer.iterator();
+ }
+
+ /**
+ * Returns an Iterator with the offline <tt>Messages</tt> whose stamp matches the specified
+ * request. The request will include the list of stamps that uniquely identifies
+ * the offline messages to retrieve. The returned offline messages will not be deleted
+ * from the server. Use {@link #deleteMessages(java.util.List)} to delete the messages.
+ *
+ * @param nodes the list of stamps that uniquely identifies offline message.
+ * @return an Iterator with the offline <tt>Messages</tt> that were received as part of
+ * this request.
+ * @throws XMPPException If the user is not allowed to make this request or the server does
+ * not support offline message retrieval.
+ */
+ public Iterator getMessages(final List nodes) throws XMPPException {
+ List messages = new ArrayList();
+ OfflineMessageRequest request = new OfflineMessageRequest();
+ for (Iterator it = nodes.iterator(); it.hasNext();) {
+ OfflineMessageRequest.Item item = new OfflineMessageRequest.Item((String) it.next());
+ item.setAction("view");
+ request.addItem(item);
+ }
+ // Filter packets looking for an answer from the server.
+ PacketFilter responseFilter = new PacketIDFilter(request.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Filter offline messages that were requested by this request
+ PacketFilter messageFilter = new AndFilter(packetFilter, new PacketFilter() {
+ public boolean accept(Packet packet) {
+ OfflineMessageInfo info = (OfflineMessageInfo) packet.getExtension("offline",
+ namespace);
+ return nodes.contains(info.getNode());
+ }
+ });
+ PacketCollector messageCollector = connection.createPacketCollector(messageFilter);
+ // Send the retrieval request to the server.
+ connection.sendPacket(request);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ } else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+
+ // Collect the received offline messages
+ Message message = (Message) messageCollector.nextResult(
+ SmackConfiguration.getPacketReplyTimeout());
+ while (message != null) {
+ messages.add(message);
+ message =
+ (Message) messageCollector.nextResult(
+ SmackConfiguration.getPacketReplyTimeout());
+ }
+ // Stop queuing offline messages
+ messageCollector.cancel();
+ return messages.iterator();
+ }
+
+ /**
+ * Returns an Iterator with all the offline <tt>Messages</tt> of the user. The returned offline
+ * messages will not be deleted from the server. Use {@link #deleteMessages(java.util.List)}
+ * to delete the messages.
+ *
+ * @return an Iterator with all the offline <tt>Messages</tt> of the user.
+ * @throws XMPPException If the user is not allowed to make this request or the server does
+ * not support offline message retrieval.
+ */
+ public Iterator getMessages() throws XMPPException {
+ List messages = new ArrayList();
+ OfflineMessageRequest request = new OfflineMessageRequest();
+ request.setFetch(true);
+ // Filter packets looking for an answer from the server.
+ PacketFilter responseFilter = new PacketIDFilter(request.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Filter offline messages that were requested by this request
+ PacketCollector messageCollector = connection.createPacketCollector(packetFilter);
+ // Send the retrieval request to the server.
+ connection.sendPacket(request);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ } else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+
+ // Collect the received offline messages
+ Message message = (Message) messageCollector.nextResult(
+ SmackConfiguration.getPacketReplyTimeout());
+ while (message != null) {
+ messages.add(message);
+ message =
+ (Message) messageCollector.nextResult(
+ SmackConfiguration.getPacketReplyTimeout());
+ }
+ // Stop queuing offline messages
+ messageCollector.cancel();
+ return messages.iterator();
+ }
+
+ /**
+ * Deletes the specified list of offline messages. The request will include the list of
+ * stamps that uniquely identifies the offline messages to delete.
+ *
+ * @param nodes the list of stamps that uniquely identifies offline message.
+ * @throws XMPPException If the user is not allowed to make this request or the server does
+ * not support offline message retrieval.
+ */
+ public void deleteMessages(List nodes) throws XMPPException {
+ OfflineMessageRequest request = new OfflineMessageRequest();
+ for (Iterator it = nodes.iterator(); it.hasNext();) {
+ OfflineMessageRequest.Item item = new OfflineMessageRequest.Item((String) it.next());
+ item.setAction("remove");
+ request.addItem(item);
+ }
+ // Filter packets looking for an answer from the server.
+ PacketFilter responseFilter = new PacketIDFilter(request.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the deletion request to the server.
+ connection.sendPacket(request);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ } else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ /**
+ * Deletes all offline messages of the user.
+ *
+ * @throws XMPPException If the user is not allowed to make this request or the server does
+ * not support offline message retrieval.
+ */
+ public void deleteMessages() throws XMPPException {
+ OfflineMessageRequest request = new OfflineMessageRequest();
+ request.setPurge(true);
+ // Filter packets looking for an answer from the server.
+ PacketFilter responseFilter = new PacketIDFilter(request.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the deletion request to the server.
+ connection.sendPacket(request);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ } else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/PrivateDataManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/PrivateDataManager.java
new file mode 100644
index 000000000..96bc6f781
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/PrivateDataManager.java
@@ -0,0 +1,345 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.packet.*;
+import org.jivesoftware.smackx.provider.*;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.Map;
+import java.util.Hashtable;
+
+/**
+ * Manages private data, which is a mechanism to allow users to store arbitrary XML
+ * data on an XMPP server. Each private data chunk is defined by a element name and
+ * XML namespace. Example private data:
+ *
+ * <pre>
+ * &lt;color xmlns="http://example.com/xmpp/color"&gt;
+ * &lt;favorite&gt;blue&lt;/blue&gt;
+ * &lt;leastFavorite&gt;puce&lt;/leastFavorite&gt;
+ * &lt;/color&gt;
+ * </pre>
+ *
+ * {@link PrivateDataProvider} instances are responsible for translating the XML into objects.
+ * If no PrivateDataProvider is registered for a given element name and namespace, then
+ * a {@link DefaultPrivateData} instance will be returned.<p>
+ *
+ * Warning: this is an non-standard protocol documented by
+ * <a href="http://www.jabber.org/jeps/jep-0049.html">JEP-49</a>. Because this is a
+ * non-standard protocol, it is subject to change.
+ *
+ * @author Matt Tucker
+ */
+public class PrivateDataManager {
+
+ /**
+ * Map of provider instances.
+ */
+ private static Map privateDataProviders = new Hashtable();
+
+ /**
+ * Returns the private data provider registered to the specified XML element name and namespace.
+ * For example, if a provider was registered to the element name "prefs" and the
+ * namespace "http://www.xmppclient.com/prefs", then the following packet would trigger
+ * the provider:
+ *
+ * <pre>
+ * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
+ * &lt;query xmlns='jabber:iq:private'&gt;
+ * &lt;prefs xmlns='http://www.xmppclient.com/prefs'&gt;
+ * &lt;value1&gt;ABC&lt;/value1&gt;
+ * &lt;value2&gt;XYZ&lt;/value2&gt;
+ * &lt;/prefs&gt;
+ * &lt;/query&gt;
+ * &lt;/iq&gt;</pre>
+ *
+ * <p>Note: this method is generally only called by the internal Smack classes.
+ *
+ * @param elementName the XML element name.
+ * @param namespace the XML namespace.
+ * @return the PrivateData provider.
+ */
+ public static PrivateDataProvider getPrivateDataProvider(String elementName, String namespace) {
+ String key = getProviderKey(elementName, namespace);
+ return (PrivateDataProvider)privateDataProviders.get(key);
+ }
+
+ /**
+ * Adds a private data provider with the specified element name and name space. The provider
+ * will override any providers loaded through the classpath.
+ *
+ * @param elementName the XML element name.
+ * @param namespace the XML namespace.
+ * @param provider the private data provider.
+ */
+ public static void addPrivateDataProvider(String elementName, String namespace,
+ PrivateDataProvider provider)
+ {
+ String key = getProviderKey(elementName, namespace);
+ privateDataProviders.put(key, provider);
+ }
+
+
+ private XMPPConnection connection;
+
+ /**
+ * The user to get and set private data for. In most cases, this value should
+ * be <tt>null</tt>, as the typical use of private data is to get and set
+ * your own private data and not others.
+ */
+ private String user;
+
+ /**
+ * Creates a new private data manager. The connection must have
+ * undergone a successful login before being used to construct an instance of
+ * this class.
+ *
+ * @param connection an XMPP connection which must have already undergone a
+ * successful login.
+ */
+ public PrivateDataManager(XMPPConnection connection) {
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Must be logged in to XMPP server.");
+ }
+ this.connection = connection;
+ }
+
+ /**
+ * Creates a new private data manager for a specific user (special case). Most
+ * servers only support getting and setting private data for the user that
+ * authenticated via the connection. However, some servers support the ability
+ * to get and set private data for other users (for example, if you are the
+ * administrator). The connection must have undergone a successful login before
+ * being used to construct an instance of this class.
+ *
+ * @param connection an XMPP connection which must have already undergone a
+ * successful login.
+ * @param user the XMPP address of the user to get and set private data for.
+ */
+ public PrivateDataManager(XMPPConnection connection, String user) {
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Must be logged in to XMPP server.");
+ }
+ this.connection = connection;
+ this.user = user;
+ }
+
+ /**
+ * Returns the private data specified by the given element name and namespace. Each chunk
+ * of private data is uniquely identified by an element name and namespace pair.<p>
+ *
+ * If a PrivateDataProvider is registered for the specified element name/namespace pair then
+ * that provider will determine the specific object type that is returned. If no provider
+ * is registered, a {@link DefaultPrivateData} instance will be returned.
+ *
+ * @param elementName the element name.
+ * @param namespace the namespace.
+ * @return the private data.
+ * @throws XMPPException if an error occurs getting the private data.
+ */
+ public PrivateData getPrivateData(final String elementName, final String namespace)
+ throws XMPPException
+ {
+ // Create an IQ packet to get the private data.
+ IQ privateDataGet = new IQ() {
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:private\">");
+ buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\"/>");
+ buf.append("</query>");
+ return buf.toString();
+ }
+ };
+ privateDataGet.setType(IQ.Type.GET);
+ // Address the packet to the other account if user has been set.
+ if (user != null) {
+ privateDataGet.setTo(user);
+ }
+
+ // Setup a listener for the reply to the set operation.
+ String packetID = privateDataGet.getPacketID();
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID));
+
+ // Send the private data.
+ connection.sendPacket(privateDataGet);
+
+ // Wait up to five seconds for a response from the server.
+ IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ return ((PrivateDataResult)response).getPrivateData();
+ }
+
+ /**
+ * Sets a private data value. Each chunk of private data is uniquely identified by an
+ * element name and namespace pair. If private data has already been set with the
+ * element name and namespace, then the new private data will overwrite the old value.
+ *
+ * @param privateData the private data.
+ * @throws XMPPException if setting the private data fails.
+ */
+ public void setPrivateData(final PrivateData privateData) throws XMPPException {
+ // Create an IQ packet to set the private data.
+ IQ privateDataSet = new IQ() {
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:private\">");
+ buf.append(privateData.toXML());
+ buf.append("</query>");
+ return buf.toString();
+ }
+ };
+ privateDataSet.setType(IQ.Type.SET);
+ // Address the packet to the other account if user has been set.
+ if (user != null) {
+ privateDataSet.setTo(user);
+ }
+
+ // Setup a listener for the reply to the set operation.
+ String packetID = privateDataSet.getPacketID();
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID));
+
+ // Send the private data.
+ connection.sendPacket(privateDataSet);
+
+ // Wait up to five seconds for a response from the server.
+ IQ response = (IQ)collector.nextResult(5000);
+ // Stop queuing results
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ // If the server replied with an error, throw an exception.
+ else if (response.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(response.getError());
+ }
+ }
+
+ /**
+ * Returns a String key for a given element name and namespace.
+ *
+ * @param elementName the element name.
+ * @param namespace the namespace.
+ * @return a unique key for the element name and namespace pair.
+ */
+ private static String getProviderKey(String elementName, String namespace) {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(elementName).append("/><").append(namespace).append("/>");
+ return buf.toString();
+ }
+
+ /**
+ * An IQ provider to parse IQ results containing private data.
+ */
+ public static class PrivateDataIQProvider implements IQProvider {
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ PrivateData privateData = null;
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ String elementName = parser.getName();
+ String namespace = parser.getNamespace();
+ // See if any objects are registered to handle this private data type.
+ PrivateDataProvider provider = getPrivateDataProvider(elementName, namespace);
+ // If there is a registered provider, use it.
+ if (provider != null) {
+ privateData = provider.parsePrivateData(parser);
+ }
+ // Otherwise, use a DefaultPrivateData instance to store the private data.
+ else {
+ DefaultPrivateData data = new DefaultPrivateData(elementName, namespace);
+ boolean finished = false;
+ while (!finished) {
+ int event = parser.next();
+ if (event == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ // If an empty element, set the value with the empty string.
+ if (parser.isEmptyElementTag()) {
+ data.setValue(name,"");
+ }
+ // Otherwise, get the the element text.
+ else {
+ event = parser.next();
+ if (event == XmlPullParser.TEXT) {
+ String value = parser.getText();
+ data.setValue(name, value);
+ }
+ }
+ }
+ else if (event == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(elementName)) {
+ finished = true;
+ }
+ }
+ }
+ privateData = data;
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+ IQ result = new PrivateDataResult(privateData);
+ return result;
+ }
+ }
+
+ /**
+ * An IQ packet to hold PrivateData GET results.
+ */
+ private static class PrivateDataResult extends IQ {
+
+ private PrivateData privateData;
+
+ PrivateDataResult(PrivateData privateData) {
+ this.privateData = privateData;
+ }
+
+ public PrivateData getPrivateData() {
+ return privateData;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:private\">");
+ if (privateData != null) {
+ privateData.toXML();
+ }
+ buf.append("</query>");
+ return buf.toString();
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RemoteRosterEntry.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RemoteRosterEntry.java
new file mode 100644
index 000000000..e8c62de31
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RemoteRosterEntry.java
@@ -0,0 +1,118 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.*;
+
+/**
+ * Represents a roster item, which consists of a JID and , their name and
+ * the groups the roster item belongs to. This roster item does not belong
+ * to the local roster. Therefore, it does not persist in the server.<p>
+ *
+ * The idea of a RemoteRosterEntry is to be used as part of a roster exchange.
+ *
+ * @author Gaston Dombiak
+ */
+public class RemoteRosterEntry {
+
+ private String user;
+ private String name;
+ private List groupNames = new ArrayList();
+
+ /**
+ * Creates a new remote roster entry.
+ *
+ * @param user the user.
+ * @param name the user's name.
+ * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the
+ * the roster entry won't belong to a group.
+ */
+ public RemoteRosterEntry(String user, String name, String [] groups) {
+ this.user = user;
+ this.name = name;
+ if (groups != null) {
+ groupNames = new ArrayList(Arrays.asList(groups));
+ }
+ }
+
+ /**
+ * Returns the user.
+ *
+ * @return the user.
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * Returns the user's name.
+ *
+ * @return the user's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns an Iterator for the group names (as Strings) that the roster entry
+ * belongs to.
+ *
+ * @return an Iterator for the group names.
+ */
+ public Iterator getGroupNames() {
+ synchronized (groupNames) {
+ return Collections.unmodifiableList(groupNames).iterator();
+ }
+ }
+
+ /**
+ * Returns a String array for the group names that the roster entry
+ * belongs to.
+ *
+ * @return a String[] for the group names.
+ */
+ public String[] getGroupArrayNames() {
+ synchronized (groupNames) {
+ return (String[])
+ (Collections
+ .unmodifiableList(groupNames)
+ .toArray(new String[groupNames.size()]));
+ }
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<item jid=\"").append(user).append("\"");
+ if (name != null) {
+ buf.append(" name=\"").append(name).append("\"");
+ }
+ buf.append(">");
+ synchronized (groupNames) {
+ for (int i = 0; i < groupNames.size(); i++) {
+ String groupName = (String) groupNames.get(i);
+ buf.append("<group>").append(groupName).append("</group>");
+ }
+ }
+ buf.append("</item>");
+ return buf.toString();
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ReportedData.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ReportedData.java
new file mode 100644
index 000000000..93e5fd140
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ReportedData.java
@@ -0,0 +1,277 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.*;
+
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smackx.packet.DataForm;
+
+/**
+ * Represents a set of data results returned as part of a search. The report is structured
+ * in columns and rows.
+ *
+ * @author Gaston Dombiak
+ */
+public class ReportedData {
+
+ private List columns = new ArrayList();
+ private List rows = new ArrayList();
+ private String title = "";
+
+ /**
+ * Returns a new ReportedData if the packet is used for reporting data and includes an
+ * extension that matches the elementName and namespace "x","jabber:x:data".
+ *
+ * @param packet the packet used for reporting data.
+ */
+ public static ReportedData getReportedDataFrom(Packet packet) {
+ // Check if the packet includes the DataForm extension
+ PacketExtension packetExtension = packet.getExtension("x","jabber:x:data");
+ if (packetExtension != null) {
+ // Check if the existing DataForm is a result of a search
+ DataForm dataForm = (DataForm) packetExtension;
+ if (dataForm.getReportedData() != null)
+ return new ReportedData(dataForm);
+ }
+ // Otherwise return null
+ return null;
+ }
+
+
+ /**
+ * Creates a new ReportedData based on the returned dataForm from a search
+ *(namespace "jabber:iq:search").
+ *
+ * @param dataForm the dataForm returned from a search (namespace "jabber:iq:search").
+ */
+ private ReportedData(DataForm dataForm) {
+ // Add the columns to the report based on the reported data fields
+ for (Iterator fields = dataForm.getReportedData().getFields(); fields.hasNext();) {
+ FormField field = (FormField)fields.next();
+ columns.add(new Column(field.getLabel(), field.getVariable(), field.getType()));
+ }
+
+ // Add the rows to the report based on the form's items
+ for (Iterator items = dataForm.getItems(); items.hasNext();) {
+ DataForm.Item item = (DataForm.Item)items.next();
+ List fieldList = new ArrayList(columns.size());
+ FormField field;
+ for (Iterator fields = item.getFields(); fields.hasNext();) {
+ field = (FormField) fields.next();
+ // The field is created with all the values of the data form's field
+ List values = new ArrayList();
+ for (Iterator it=field.getValues(); it.hasNext();) {
+ values.add(it.next());
+ }
+ fieldList.add(new Field(field.getVariable(), values));
+ }
+ rows.add(new Row(fieldList));
+ }
+
+ // Set the report's title
+ this.title = dataForm.getTitle();
+ }
+
+
+ public ReportedData(){
+ // Allow for model creation of ReportedData.
+ }
+
+ /**
+ * Adds a new <code>Row</code>.
+ * @param row the new row to add.
+ */
+ public void addRow(Row row){
+ rows.add(row);
+ }
+
+ /**
+ * Adds a new <code>Column</code>
+ * @param column the column to add.
+ */
+ public void addColumn(Column column){
+ columns.add(column);
+ }
+
+
+ /**
+ * Returns an Iterator for the rows returned from a search.
+ *
+ * @return an Iterator for the rows returned from a search.
+ */
+ public Iterator getRows() {
+ return Collections.unmodifiableList(new ArrayList(rows)).iterator();
+ }
+
+ /**
+ * Returns an Iterator for the columns returned from a search.
+ *
+ * @return an Iterator for the columns returned from a search.
+ */
+ public Iterator getColumns() {
+ return Collections.unmodifiableList(new ArrayList(columns)).iterator();
+ }
+
+
+ /**
+ * Returns the report's title. It is similar to the title on a web page or an X
+ * window.
+ *
+ * @return title of the report.
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ *
+ * Represents the columns definition of the reported data.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Column {
+ private String label;
+ private String variable;
+ private String type;
+
+ /**
+ * Creates a new column with the specified definition.
+ *
+ * @param label the columns's label.
+ * @param variable the variable name of the column.
+ * @param type the format for the returned data.
+ */
+ public Column(String label, String variable, String type) {
+ this.label = label;
+ this.variable = variable;
+ this.type = type;
+ }
+
+ /**
+ * Returns the column's label.
+ *
+ * @return label of the column.
+ */
+ public String getLabel() {
+ return label;
+ }
+
+
+ /**
+ * Returns the column's data format. Valid formats are:
+ *
+ * <ul>
+ * <li>text-single -> single line or word of text
+ * <li>text-private -> instead of showing the user what they typed, you show ***** to
+ * protect it
+ * <li>text-multi -> multiple lines of text entry
+ * <li>list-single -> given a list of choices, pick one
+ * <li>list-multi -> given a list of choices, pick one or more
+ * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0
+ * <li>fixed -> fixed for putting in text to show sections, or just advertise your web
+ * site in the middle of the form
+ * <li>hidden -> is not given to the user at all, but returned with the questionnaire
+ * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based
+ * on the rules for a JID.
+ * <li>jid-multi -> multiple entries for JIDs
+ * </ul>
+ *
+ * @return format for the returned data.
+ */
+ public String getType() {
+ return type;
+ }
+
+
+ /**
+ * Returns the variable name that the column is showing.
+ *
+ * @return the variable name of the column.
+ */
+ public String getVariable() {
+ return variable;
+ }
+
+
+ }
+
+ public static class Row {
+ private List fields = new ArrayList();
+
+ public Row(List fields) {
+ this.fields = fields;
+ }
+
+ /**
+ * Returns the values of the field whose variable matches the requested variable.
+ *
+ * @param variable the variable to match.
+ * @return the values of the field whose variable matches the requested variable.
+ */
+ public Iterator getValues(String variable) {
+ for(Iterator it=getFields();it.hasNext();) {
+ Field field = (Field) it.next();
+ if (variable.equalsIgnoreCase(field.getVariable())) {
+ return field.getValues();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the fields that define the data that goes with the item.
+ *
+ * @return the fields that define the data that goes with the item.
+ */
+ private Iterator getFields() {
+ return Collections.unmodifiableList(new ArrayList(fields)).iterator();
+ }
+ }
+
+ public static class Field {
+ private String variable;
+ private List values;
+
+ public Field(String variable, List values) {
+ this.variable = variable;
+ this.values = values;
+ }
+
+ /**
+ * Returns the variable name that the field represents.
+ *
+ * @return the variable name of the field.
+ */
+ public String getVariable() {
+ return variable;
+ }
+
+ /**
+ * Returns an iterator on the values reported as part of the search.
+ *
+ * @return the returned values of the search.
+ */
+ public Iterator getValues() {
+ return Collections.unmodifiableList(values).iterator();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeListener.java
new file mode 100644
index 000000000..2c7460b62
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeListener.java
@@ -0,0 +1,42 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.Iterator;
+
+/**
+ *
+ * A listener that is fired anytime a roster exchange is received.
+ *
+ * @author Gaston Dombiak
+ */
+public interface RosterExchangeListener {
+
+ /**
+ * Called when roster entries are received as part of a roster exchange.
+ *
+ * @param from the user that sent the entries.
+ * @param remoteRosterEntries the entries sent by the user. The entries are instances of
+ * RemoteRosterEntry.
+ */
+ public void entriesReceived(String from, Iterator remoteRosterEntries);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeManager.java
new file mode 100644
index 000000000..66c4f477f
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeManager.java
@@ -0,0 +1,177 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.*;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smackx.packet.RosterExchange;
+
+/**
+ *
+ * Manages Roster exchanges. A RosterExchangeManager provides a high level access to send
+ * rosters, roster groups and roster entries to XMPP clients. It also provides an easy way
+ * to hook up custom logic when entries are received from another XMPP client through
+ * RosterExchangeListeners.
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchangeManager {
+
+ private List rosterExchangeListeners = new ArrayList();
+
+ private XMPPConnection con;
+
+ private PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:roster");
+ private PacketListener packetListener;
+
+ /**
+ * Creates a new roster exchange manager.
+ *
+ * @param con an XMPPConnection.
+ */
+ public RosterExchangeManager(XMPPConnection con) {
+ this.con = con;
+ init();
+ }
+
+ /**
+ * Adds a listener to roster exchanges. The listener will be fired anytime roster entries
+ * are received from remote XMPP clients.
+ *
+ * @param rosterExchangeListener a roster exchange listener.
+ */
+ public void addRosterListener(RosterExchangeListener rosterExchangeListener) {
+ synchronized (rosterExchangeListeners) {
+ if (!rosterExchangeListeners.contains(rosterExchangeListener)) {
+ rosterExchangeListeners.add(rosterExchangeListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener from roster exchanges. The listener will be fired anytime roster
+ * entries are received from remote XMPP clients.
+ *
+ * @param rosterExchangeListener a roster exchange listener..
+ */
+ public void removeRosterListener(RosterExchangeListener rosterExchangeListener) {
+ synchronized (rosterExchangeListeners) {
+ rosterExchangeListeners.remove(rosterExchangeListener);
+ }
+ }
+
+ /**
+ * Sends a roster to userID. All the entries of the roster will be sent to the
+ * target user.
+ *
+ * @param roster the roster to send
+ * @param targetUserID the user that will receive the roster entries
+ */
+ public void send(Roster roster, String targetUserID) {
+ // Create a new message to send the roster
+ Message msg = new Message(targetUserID);
+ // Create a RosterExchange Package and add it to the message
+ RosterExchange rosterExchange = new RosterExchange(roster);
+ msg.addExtension(rosterExchange);
+
+ // Send the message that contains the roster
+ con.sendPacket(msg);
+ }
+
+ /**
+ * Sends a roster entry to userID.
+ *
+ * @param rosterEntry the roster entry to send
+ * @param targetUserID the user that will receive the roster entries
+ */
+ public void send(RosterEntry rosterEntry, String targetUserID) {
+ // Create a new message to send the roster
+ Message msg = new Message(targetUserID);
+ // Create a RosterExchange Package and add it to the message
+ RosterExchange rosterExchange = new RosterExchange();
+ rosterExchange.addRosterEntry(rosterEntry);
+ msg.addExtension(rosterExchange);
+
+ // Send the message that contains the roster
+ con.sendPacket(msg);
+ }
+
+ /**
+ * Sends a roster group to userID. All the entries of the group will be sent to the
+ * target user.
+ *
+ * @param rosterGroup the roster group to send
+ * @param targetUserID the user that will receive the roster entries
+ */
+ public void send(RosterGroup rosterGroup, String targetUserID) {
+ // Create a new message to send the roster
+ Message msg = new Message(targetUserID);
+ // Create a RosterExchange Package and add it to the message
+ RosterExchange rosterExchange = new RosterExchange();
+ for (Iterator it = rosterGroup.getEntries(); it.hasNext();)
+ rosterExchange.addRosterEntry((RosterEntry) it.next());
+ msg.addExtension(rosterExchange);
+
+ // Send the message that contains the roster
+ con.sendPacket(msg);
+ }
+
+ /**
+ * Fires roster exchange listeners.
+ */
+ private void fireRosterExchangeListeners(String from, Iterator remoteRosterEntries) {
+ RosterExchangeListener[] listeners = null;
+ synchronized (rosterExchangeListeners) {
+ listeners = new RosterExchangeListener[rosterExchangeListeners.size()];
+ rosterExchangeListeners.toArray(listeners);
+ }
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].entriesReceived(from, remoteRosterEntries);
+ }
+ }
+
+ private void init() {
+ // Listens for all roster exchange packets and fire the roster exchange listeners.
+ packetListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ Message message = (Message) packet;
+ RosterExchange rosterExchange =
+ (RosterExchange) message.getExtension("x", "jabber:x:roster");
+ // Fire event for roster exchange listeners
+ fireRosterExchangeListeners(message.getFrom(), rosterExchange.getRosterEntries());
+ };
+
+ };
+ con.addPacketListener(packetListener, packetFilter);
+ }
+
+ public void destroy() {
+ if (con != null)
+ con.removePacketListener(packetListener);
+
+ }
+ public void finalize() {
+ destroy();
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java
new file mode 100644
index 000000000..94b2ea4cf
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java
@@ -0,0 +1,483 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.*;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smackx.packet.*;
+
+/**
+ * Manages discovery of services in XMPP entities. This class provides:
+ * <ol>
+ * <li>A registry of supported features in this XMPP entity.
+ * <li>Automatic response when this XMPP entity is queried for information.
+ * <li>Ability to discover items and information of remote XMPP entities.
+ * <li>Ability to publish publicly available items.
+ * </ol>
+ *
+ * @author Gaston Dombiak
+ */
+public class ServiceDiscoveryManager {
+
+ private static String identityName = "Smack";
+ private static String identityType = "pc";
+
+ private static Map instances = new Hashtable();
+
+ private XMPPConnection connection;
+ private List features = new ArrayList();
+ private Map nodeInformationProviders = new Hashtable();
+
+ // Create a new ServiceDiscoveryManager on every established connection
+ static {
+ XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() {
+ public void connectionEstablished(XMPPConnection connection) {
+ new ServiceDiscoveryManager(connection);
+ }
+ });
+ }
+
+ /**
+ * Creates a new ServiceDiscoveryManager for a given XMPPConnection. This means that the
+ * service manager will respond to any service discovery request that the connection may
+ * receive.
+ *
+ * @param connection the connection to which a ServiceDiscoveryManager is going to be created.
+ */
+ public ServiceDiscoveryManager(XMPPConnection connection) {
+ this.connection = connection;
+ init();
+ }
+
+ /**
+ * Returns the ServiceDiscoveryManager instance associated with a given XMPPConnection.
+ *
+ * @param connection the connection used to look for the proper ServiceDiscoveryManager.
+ * @return the ServiceDiscoveryManager associated with a given XMPPConnection.
+ */
+ public static ServiceDiscoveryManager getInstanceFor(XMPPConnection connection) {
+ return (ServiceDiscoveryManager) instances.get(connection);
+ }
+
+ /**
+ * Returns the name of the client that will be returned when asked for the client identity
+ * in a disco request. The name could be any value you need to identity this client.
+ *
+ * @return the name of the client that will be returned when asked for the client identity
+ * in a disco request.
+ */
+ public static String getIdentityName() {
+ return identityName;
+ }
+
+ /**
+ * Sets the name of the client that will be returned when asked for the client identity
+ * in a disco request. The name could be any value you need to identity this client.
+ *
+ * @param name the name of the client that will be returned when asked for the client identity
+ * in a disco request.
+ */
+ public static void setIdentityName(String name) {
+ identityName = name;
+ }
+
+ /**
+ * 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
+ * the possible types: <a href="http://www.jabber.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
+ *
+ * @return the type of client that will be returned when asked for the client identity in a
+ * disco request.
+ */
+ public static String getIdentityType() {
+ return identityType;
+ }
+
+ /**
+ * Sets 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
+ * the possible types: <a href="http://www.jabber.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
+ *
+ * @param type the type of client that will be returned when asked for the client identity in a
+ * disco request.
+ */
+ public static void setIdentityType(String type) {
+ identityType = type;
+ }
+
+ /**
+ * Initializes the packet listeners of the connection that will answer to any
+ * service discovery request.
+ */
+ private void init() {
+ // Register the new instance and associate it with the connection
+ instances.put(connection, this);
+ // Add a listener to the connection that removes the registered instance when
+ // the connection is closed
+ connection.addConnectionListener(new ConnectionListener() {
+ public void connectionClosed() {
+ // Unregister this instance since the connection has been closed
+ instances.remove(connection);
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ // Unregister this instance since the connection has been closed
+ instances.remove(connection);
+ }
+ });
+
+ // Listen for disco#items requests and answer with an empty result
+ PacketFilter packetFilter = new PacketTypeFilter(DiscoverItems.class);
+ PacketListener packetListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ DiscoverItems discoverItems = (DiscoverItems) packet;
+ // Send back the items defined in the client if the request is of type GET
+ if (discoverItems != null && discoverItems.getType() == IQ.Type.GET) {
+ DiscoverItems response = new DiscoverItems();
+ response.setType(IQ.Type.RESULT);
+ response.setTo(discoverItems.getFrom());
+ response.setPacketID(discoverItems.getPacketID());
+
+ // Add the defined items related to the requested node. Look for
+ // the NodeInformationProvider associated with the requested node.
+ if (getNodeInformationProvider(discoverItems.getNode()) != null) {
+ Iterator items =
+ getNodeInformationProvider(discoverItems.getNode()).getNodeItems();
+ while (items.hasNext()) {
+ response.addItem((DiscoverItems.Item) items.next());
+ }
+ } else if(discoverItems.getNode() != null) {
+ // Return an <item-not-found/> error since the client
+ // doesn't contain the specified node
+ response.setNode(discoverItems.getNode());
+ response.setType(IQ.Type.ERROR);
+ response.setError(new XMPPError(404, "item-not-found"));
+ }
+ connection.sendPacket(response);
+ }
+ }
+ };
+ connection.addPacketListener(packetListener, packetFilter);
+
+ // Listen for disco#info requests and answer the client's supported features
+ // To add a new feature as supported use the #addFeature message
+ packetFilter = new PacketTypeFilter(DiscoverInfo.class);
+ packetListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ DiscoverInfo discoverInfo = (DiscoverInfo) packet;
+ // Answer the client's supported features if the request is of the GET type
+ if (discoverInfo != null && discoverInfo.getType() == IQ.Type.GET) {
+ DiscoverInfo response = new DiscoverInfo();
+ response.setType(IQ.Type.RESULT);
+ response.setTo(discoverInfo.getFrom());
+ response.setPacketID(discoverInfo.getPacketID());
+ // Add the client's identity and features only if "node" is null
+ if (discoverInfo.getNode() == null) {
+ // Set this client identity
+ DiscoverInfo.Identity identity = new DiscoverInfo.Identity("client",
+ getIdentityName());
+ identity.setType(getIdentityType());
+ response.addIdentity(identity);
+ // Add the registered features to the response
+ synchronized (features) {
+ for (Iterator it = getFeatures(); it.hasNext();) {
+ response.addFeature((String) it.next());
+ }
+ }
+ }
+ else {
+ // Return an <item-not-found/> error since a client doesn't have nodes
+ response.setNode(discoverInfo.getNode());
+ response.setType(IQ.Type.ERROR);
+ response.setError(new XMPPError(404, "item-not-found"));
+ }
+ connection.sendPacket(response);
+ }
+ }
+ };
+ connection.addPacketListener(packetListener, packetFilter);
+ }
+
+ /**
+ * Returns the NodeInformationProvider responsible for providing information
+ * (ie items) related to a given node or <tt>null</null> if none.<p>
+ *
+ * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
+ * NodeInformationProvider will provide information about the rooms where the user has joined.
+ *
+ * @param node the node that contains items associated with an entity not addressable as a JID.
+ * @return the NodeInformationProvider responsible for providing information related
+ * to a given node.
+ */
+ private NodeInformationProvider getNodeInformationProvider(String node) {
+ if (node == null) {
+ return null;
+ }
+ return (NodeInformationProvider) nodeInformationProviders.get(node);
+ }
+
+ /**
+ * Sets the NodeInformationProvider responsible for providing information
+ * (ie items) related to a given node. Every time this client receives a disco request
+ * regarding the items of a given node, the provider associated to that node will be the
+ * responsible for providing the requested information.<p>
+ *
+ * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
+ * NodeInformationProvider will provide information about the rooms where the user has joined.
+ *
+ * @param node the node whose items will be provided by the NodeInformationProvider.
+ * @param listener the NodeInformationProvider responsible for providing items related
+ * to the node.
+ */
+ public void setNodeInformationProvider(String node, NodeInformationProvider listener) {
+ nodeInformationProviders.put(node, listener);
+ }
+
+ /**
+ * Removes the NodeInformationProvider responsible for providing information
+ * (ie items) related to a given node. This means that no more information will be
+ * available for the specified node.
+ *
+ * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
+ * NodeInformationProvider will provide information about the rooms where the user has joined.
+ *
+ * @param node the node to remove the associated NodeInformationProvider.
+ */
+ public void removeNodeInformationProvider(String node) {
+ nodeInformationProviders.remove(node);
+ }
+
+ /**
+ * Returns the supported features by this XMPP entity.
+ *
+ * @return an Iterator on the supported features by this XMPP entity.
+ */
+ public Iterator getFeatures() {
+ synchronized (features) {
+ return Collections.unmodifiableList(new ArrayList(features)).iterator();
+ }
+ }
+
+ /**
+ * Registers that a new feature is supported by this XMPP entity. When this client is
+ * queried for its information the registered features will be answered.<p>
+ *
+ * Since no packet is actually sent to the server it is safe to perform this operation
+ * before logging to the server. In fact, you may want to configure the supported features
+ * before logging to the server so that the information is already available if it is required
+ * upon login.
+ *
+ * @param feature the feature to register as supported.
+ */
+ public void addFeature(String feature) {
+ synchronized (features) {
+ features.add(feature);
+ }
+ }
+
+ /**
+ * Removes the specified feature from the supported features by this XMPP entity.<p>
+ *
+ * Since no packet is actually sent to the server it is safe to perform this operation
+ * before logging to the server.
+ *
+ * @param feature the feature to remove from the supported features.
+ */
+ public void removeFeature(String feature) {
+ synchronized (features) {
+ features.remove(feature);
+ }
+ }
+
+ /**
+ * Returns true if the specified feature is registered in the ServiceDiscoveryManager.
+ *
+ * @param feature the feature to look for.
+ * @return a boolean indicating if the specified featured is registered or not.
+ */
+ public boolean includesFeature(String feature) {
+ synchronized (features) {
+ return features.contains(feature);
+ }
+ }
+
+ /**
+ * Returns the discovered information of a given XMPP entity addressed by its JID.
+ *
+ * @param entityID the address of the XMPP entity.
+ * @return the discovered information.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public DiscoverInfo discoverInfo(String entityID) throws XMPPException {
+ return discoverInfo(entityID, null);
+ }
+
+ /**
+ * Returns the discovered information of a given XMPP entity addressed by its JID and
+ * note attribute. Use this message only when trying to query information which is not
+ * directly addressable.
+ *
+ * @param entityID the address of the XMPP entity.
+ * @param node the attribute that supplements the 'jid' attribute.
+ * @return the discovered information.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public DiscoverInfo discoverInfo(String entityID, String node) throws XMPPException {
+ // Discover the entity's info
+ DiscoverInfo disco = new DiscoverInfo();
+ disco.setType(IQ.Type.GET);
+ disco.setTo(entityID);
+ disco.setNode(node);
+
+ // Create a packet collector to listen for a response.
+ PacketCollector collector =
+ connection.createPacketCollector(new PacketIDFilter(disco.getPacketID()));
+
+ connection.sendPacket(disco);
+
+ // Wait up to 5 seconds for a result.
+ IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ return (DiscoverInfo) result;
+ }
+
+ /**
+ * Returns the discovered items of a given XMPP entity addressed by its JID.
+ *
+ * @param entityID the address of the XMPP entity.
+ * @return the discovered information.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public DiscoverItems discoverItems(String entityID) throws XMPPException {
+ return discoverItems(entityID, null);
+ }
+
+ /**
+ * Returns the discovered items of a given XMPP entity addressed by its JID and
+ * note attribute. Use this message only when trying to query information which is not
+ * directly addressable.
+ *
+ * @param entityID the address of the XMPP entity.
+ * @param node the attribute that supplements the 'jid' attribute.
+ * @return the discovered items.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public DiscoverItems discoverItems(String entityID, String node) throws XMPPException {
+ // Discover the entity's items
+ DiscoverItems disco = new DiscoverItems();
+ disco.setType(IQ.Type.GET);
+ disco.setTo(entityID);
+ disco.setNode(node);
+
+ // Create a packet collector to listen for a response.
+ PacketCollector collector =
+ connection.createPacketCollector(new PacketIDFilter(disco.getPacketID()));
+
+ connection.sendPacket(disco);
+
+ // Wait up to 5 seconds for a result.
+ IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ return (DiscoverItems) result;
+ }
+
+ /**
+ * Returns true if the server supports publishing of items. A client may wish to publish items
+ * to the server so that the server can provide items associated to the client. These items will
+ * be returned by the server whenever the server receives a disco request targeted to the bare
+ * address of the client (i.e. user@host.com).
+ *
+ * @param entityID the address of the XMPP entity.
+ * @return true if the server supports publishing of items.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public boolean canPublishItems(String entityID) throws XMPPException {
+ DiscoverInfo info = discoverInfo(entityID);
+ return info.containsFeature("http://jabber.org/protocol/disco#publish");
+ }
+
+ /**
+ * Publishes new items to a parent entity. The item elements to publish MUST have at least
+ * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which
+ * specifies the action being taken for that item. Possible action values are: "update" and
+ * "remove".
+ *
+ * @param entityID the address of the XMPP entity.
+ * @param discoverItems the DiscoveryItems to publish.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public void publishItems(String entityID, DiscoverItems discoverItems)
+ throws XMPPException {
+ publishItems(entityID, null, discoverItems);
+ }
+
+ /**
+ * Publishes new items to a parent entity and node. The item elements to publish MUST have at
+ * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which
+ * specifies the action being taken for that item. Possible action values are: "update" and
+ * "remove".
+ *
+ * @param entityID the address of the XMPP entity.
+ * @param node the attribute that supplements the 'jid' attribute.
+ * @param discoverItems the DiscoveryItems to publish.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public void publishItems(String entityID, String node, DiscoverItems discoverItems)
+ throws XMPPException {
+ discoverItems.setType(IQ.Type.SET);
+ discoverItems.setTo(entityID);
+ discoverItems.setNode(node);
+
+ // Create a packet collector to listen for a response.
+ PacketCollector collector =
+ connection.createPacketCollector(new PacketIDFilter(discoverItems.getPacketID()));
+
+ connection.sendPacket(discoverItems);
+
+ // Wait up to 5 seconds for a result.
+ IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/SharedGroupManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/SharedGroupManager.java
new file mode 100644
index 000000000..f69f30708
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/SharedGroupManager.java
@@ -0,0 +1,53 @@
+package org.jivesoftware.smackx;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.packet.SharedGroupsInfo;
+
+import java.util.List;
+
+/**
+ * A SharedGroupManager provides services for discovering the shared groups where a user belongs.<p>
+ *
+ * Important note: This functionality is not part of the XMPP spec and it will only work
+ * with Wildfire.
+ *
+ * @author Gaston Dombiak
+ */
+public class SharedGroupManager {
+
+ /**
+ * Returns the collection that will contain the name of the shared groups where the user
+ * logged in with the specified session belongs.
+ *
+ * @param connection connection to use to get the user's shared groups.
+ * @return collection with the shared groups' name of the logged user.
+ */
+ public static List getSharedGroups(XMPPConnection connection) throws XMPPException {
+ // Discover the shared groups of the logged user
+ SharedGroupsInfo info = new SharedGroupsInfo();
+ info.setType(IQ.Type.GET);
+
+ // Create a packet collector to listen for a response.
+ PacketCollector collector =
+ connection.createPacketCollector(new PacketIDFilter(info.getPacketID()));
+
+ connection.sendPacket(info);
+
+ // Wait up to 5 seconds for a result.
+ IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ return ((SharedGroupsInfo) result).getGroups();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLManager.java
new file mode 100644
index 000000000..064c03b43
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLManager.java
@@ -0,0 +1,141 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import java.util.Iterator;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smackx.packet.*;
+
+/**
+ * Manages XHTML formatted texts within messages. A XHTMLManager provides a high level access to
+ * get and set XHTML bodies to messages, enable and disable XHTML support and check if remote XMPP
+ * clients support XHTML.
+ *
+ * @author Gaston Dombiak
+ */
+public class XHTMLManager {
+
+ private final static String namespace = "http://jabber.org/protocol/xhtml-im";
+
+ // Enable the XHTML support on every established connection
+ // The ServiceDiscoveryManager class should have been already initialized
+ static {
+ XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() {
+ public void connectionEstablished(XMPPConnection connection) {
+ XHTMLManager.setServiceEnabled(connection, true);
+ }
+ });
+ }
+
+ /**
+ * Returns an Iterator for the XHTML bodies in the message. Returns null if
+ * the message does not contain an XHTML extension.
+ *
+ * @param message an XHTML message
+ * @return an Iterator for the bodies in the message or null if none.
+ */
+ public static Iterator getBodies(Message message) {
+ XHTMLExtension xhtmlExtension = (XHTMLExtension) message.getExtension("html", namespace);
+ if (xhtmlExtension != null)
+ return xhtmlExtension.getBodies();
+ else
+ return null;
+ }
+
+ /**
+ * Adds an XHTML body to the message.
+ *
+ * @param message the message that will receive the XHTML body
+ * @param body the string to add as an XHTML body to the message
+ */
+ public static void addBody(Message message, String body) {
+ XHTMLExtension xhtmlExtension = (XHTMLExtension) message.getExtension("html", namespace);
+ if (xhtmlExtension == null) {
+ // Create an XHTMLExtension and add it to the message
+ xhtmlExtension = new XHTMLExtension();
+ message.addExtension(xhtmlExtension);
+ }
+ // Add the required bodies to the message
+ xhtmlExtension.addBody(body);
+ }
+
+ /**
+ * Returns true if the message contains an XHTML extension.
+ *
+ * @param message the message to check if contains an XHTML extentsion or not
+ * @return a boolean indicating whether the message is an XHTML message
+ */
+ public static boolean isXHTMLMessage(Message message) {
+ return message.getExtension("html", namespace) != null;
+ }
+
+ /**
+ * Enables or disables the XHTML support on a given connection.<p>
+ *
+ * Before starting to send XHTML messages to a user, check that the user can handle XHTML
+ * messages. Enable the XHTML support to indicate that this client handles XHTML messages.
+ *
+ * @param connection the connection where the service will be enabled or disabled
+ * @param enabled indicates if the service will be enabled or disabled
+ */
+ public synchronized static void setServiceEnabled(XMPPConnection connection, boolean enabled) {
+ if (isServiceEnabled(connection) == enabled)
+ return;
+
+ if (enabled) {
+ ServiceDiscoveryManager.getInstanceFor(connection).addFeature(namespace);
+ }
+ else {
+ ServiceDiscoveryManager.getInstanceFor(connection).removeFeature(namespace);
+ }
+ }
+
+ /**
+ * Returns true if the XHTML support is enabled for the given connection.
+ *
+ * @param connection the connection to look for XHTML support
+ * @return a boolean indicating if the XHTML support is enabled for the given connection
+ */
+ public static boolean isServiceEnabled(XMPPConnection connection) {
+ return ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(namespace);
+ }
+
+ /**
+ * Returns true if the specified user handles XHTML messages.
+ *
+ * @param connection the connection to use to perform the service discovery
+ * @param userID the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com
+ * @return a boolean indicating whether the specified user handles XHTML messages
+ */
+ public static boolean isServiceEnabled(XMPPConnection connection, String userID) {
+ try {
+ DiscoverInfo result =
+ ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(userID);
+ return result.containsFeature(namespace);
+ }
+ catch (XMPPException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLText.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLText.java
new file mode 100644
index 000000000..ee67e2880
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLText.java
@@ -0,0 +1,429 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * An XHTMLText represents formatted text. This class also helps to build valid
+ * XHTML tags.
+ *
+ * @author Gaston Dombiak
+ */
+public class XHTMLText {
+
+ private StringBuffer text = new StringBuffer(30);
+
+ /**
+ * Creates a new XHTMLText with body tag params.
+ *
+ * @param style the XHTML style of the body
+ * @param lang the language of the body
+ */
+ public XHTMLText(String style, String lang) {
+ appendOpenBodyTag(style, lang);
+ }
+
+ /**
+ * Appends a tag that indicates that an anchor section begins.
+ *
+ * @param href indicates the URL being linked to
+ * @param style the XHTML style of the anchor
+ */
+ public void appendOpenAnchorTag(String href, String style) {
+ StringBuffer sb = new StringBuffer("<a");
+ if (href != null) {
+ sb.append(" href=\"");
+ sb.append(href);
+ sb.append("\"");
+ }
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates that an anchor section ends.
+ *
+ */
+ public void appendCloseAnchorTag() {
+ text.append("</a>");
+ }
+
+ /**
+ * Appends a tag that indicates that a blockquote section begins.
+ *
+ * @param style the XHTML style of the blockquote
+ */
+ public void appendOpenBlockQuoteTag(String style) {
+ StringBuffer sb = new StringBuffer("<blockquote");
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates that a blockquote section ends.
+ *
+ */
+ public void appendCloseBlockQuoteTag() {
+ text.append("</blockquote>");
+ }
+
+ /**
+ * Appends a tag that indicates that a body section begins.
+ *
+ * @param style the XHTML style of the body
+ * @param lang the language of the body
+ */
+ private void appendOpenBodyTag(String style, String lang) {
+ StringBuffer sb = new StringBuffer("<body");
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ if (lang != null) {
+ sb.append(" xml:lang=\"");
+ sb.append(lang);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates that a body section ends.
+ *
+ */
+ private String closeBodyTag() {
+ return "</body>";
+ }
+
+ /**
+ * Appends a tag that inserts a single carriage return.
+ *
+ */
+ public void appendBrTag() {
+ text.append("<br>");
+ }
+
+ /**
+ * Appends a tag that indicates a reference to work, such as a book, report or web site.
+ *
+ */
+ public void appendOpenCiteTag() {
+ text.append("<cite>");
+ }
+
+ /**
+ * Appends a tag that indicates text that is the code for a program.
+ *
+ */
+ public void appendOpenCodeTag() {
+ text.append("<code>");
+ }
+
+ /**
+ * Appends a tag that indicates end of text that is the code for a program.
+ *
+ */
+ public void appendCloseCodeTag() {
+ text.append("</code>");
+ }
+
+ /**
+ * Appends a tag that indicates emphasis.
+ *
+ */
+ public void appendOpenEmTag() {
+ text.append("<em>");
+ }
+
+ /**
+ * Appends a tag that indicates end of emphasis.
+ *
+ */
+ public void appendCloseEmTag() {
+ text.append("</em>");
+ }
+
+ /**
+ * Appends a tag that indicates a header, a title of a section of the message.
+ *
+ * @param level the level of the Header. It should be a value between 1 and 3
+ * @param style the XHTML style of the blockquote
+ */
+ public void appendOpenHeaderTag(int level, String style) {
+ if (level > 3 || level < 1) {
+ return;
+ }
+ StringBuffer sb = new StringBuffer("<h");
+ sb.append(level);
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates that a header section ends.
+ *
+ * @param level the level of the Header. It should be a value between 1 and 3
+ */
+ public void appendCloseHeaderTag(int level) {
+ if (level > 3 || level < 1) {
+ return;
+ }
+ StringBuffer sb = new StringBuffer("</h");
+ sb.append(level);
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates an image.
+ *
+ * @param align how text should flow around the picture
+ * @param alt the text to show if you don't show the picture
+ * @param height how tall is the picture
+ * @param src where to get the picture
+ * @param width how wide is the picture
+ */
+ public void appendImageTag(String align, String alt, String height, String src, String width) {
+ StringBuffer sb = new StringBuffer("<img");
+ if (align != null) {
+ sb.append(" align=\"");
+ sb.append(align);
+ sb.append("\"");
+ }
+ if (alt != null) {
+ sb.append(" alt=\"");
+ sb.append(alt);
+ sb.append("\"");
+ }
+ if (height != null) {
+ sb.append(" height=\"");
+ sb.append(height);
+ sb.append("\"");
+ }
+ if (src != null) {
+ sb.append(" src=\"");
+ sb.append(src);
+ sb.append("\"");
+ }
+ if (width != null) {
+ sb.append(" width=\"");
+ sb.append(width);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates the start of a new line item within a list.
+ *
+ * @param style the style of the line item
+ */
+ public void appendLineItemTag(String style) {
+ StringBuffer sb = new StringBuffer("<li");
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that creates an ordered list. "Ordered" means that the order of the items
+ * in the list is important. To show this, browsers automatically number the list.
+ *
+ * @param style the style of the ordered list
+ */
+ public void appendOpenOrderedListTag(String style) {
+ StringBuffer sb = new StringBuffer("<ol");
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates that an ordered list section ends.
+ *
+ */
+ public void appendCloseOrderedListTag() {
+ text.append("</ol>");
+ }
+
+ /**
+ * Appends a tag that creates an unordered list. The unordered part means that the items
+ * in the list are not in any particular order.
+ *
+ * @param style the style of the unordered list
+ */
+ public void appendOpenUnorderedListTag(String style) {
+ StringBuffer sb = new StringBuffer("<ul");
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates that an unordered list section ends.
+ *
+ */
+ public void appendCloseUnorderedListTag() {
+ text.append("</ul>");
+ }
+
+ /**
+ * Appends a tag that indicates the start of a new paragraph. This is usually rendered
+ * with two carriage returns, producing a single blank line in between the two paragraphs.
+ *
+ * @param style the style of the paragraph
+ */
+ public void appendOpenParagraphTag(String style) {
+ StringBuffer sb = new StringBuffer("<p");
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates the end of a new paragraph. This is usually rendered
+ * with two carriage returns, producing a single blank line in between the two paragraphs.
+ *
+ */
+ public void appendCloseParagraphTag() {
+ text.append("</p>");
+ }
+
+ /**
+ * Appends a tag that indicates that an inlined quote section begins.
+ *
+ * @param style the style of the inlined quote
+ */
+ public void appendOpenInlinedQuoteTag(String style) {
+ StringBuffer sb = new StringBuffer("<q");
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates that an inlined quote section ends.
+ *
+ */
+ public void appendCloseInlinedQuoteTag() {
+ text.append("</q>");
+ }
+
+ /**
+ * Appends a tag that allows to set the fonts for a span of text.
+ *
+ * @param style the style for a span of text
+ */
+ public void appendOpenSpanTag(String style) {
+ StringBuffer sb = new StringBuffer("<span");
+ if (style != null) {
+ sb.append(" style=\"");
+ sb.append(style);
+ sb.append("\"");
+ }
+ sb.append(">");
+ text.append(sb.toString());
+ }
+
+ /**
+ * Appends a tag that indicates that a span section ends.
+ *
+ */
+ public void appendCloseSpanTag() {
+ text.append("</span>");
+ }
+
+ /**
+ * Appends a tag that indicates text which should be more forceful than surrounding text.
+ *
+ */
+ public void appendOpenStrongTag() {
+ text.append("<strong>");
+ }
+
+ /**
+ * Appends a tag that indicates that a strong section ends.
+ *
+ */
+ public void appendCloseStrongTag() {
+ text.append("</strong>");
+ }
+
+ /**
+ * Appends a given text to the XHTMLText.
+ *
+ * @param textToAppend the text to append
+ */
+ public void append(String textToAppend) {
+ text.append(StringUtils.escapeForXML(textToAppend));
+ }
+
+ /**
+ * Returns the text of the XHTMLText.
+ *
+ * Note: Automatically adds the closing body tag.
+ *
+ * @return the text of the XHTMLText
+ */
+ public String toString() {
+ return text.toString().concat(closeBodyTag());
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebugger.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebugger.java
new file mode 100644
index 000000000..3e1a078cd
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebugger.java
@@ -0,0 +1,985 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.debugger;
+
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.debugger.SmackDebugger;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.util.*;
+
+import javax.swing.*;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.text.BadLocationException;
+import javax.xml.transform.*;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+import java.awt.*;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * The EnhancedDebugger is a debugger that allows to debug sent, received and interpreted messages
+ * but also provides the ability to send ad-hoc messages composed by the user.<p>
+ * <p/>
+ * A new EnhancedDebugger will be created for each connection to debug. All the EnhancedDebuggers
+ * will be shown in the same debug window provided by the class EnhancedDebuggerWindow.
+ *
+ * @author Gaston Dombiak
+ */
+public class EnhancedDebugger implements SmackDebugger {
+
+ private static final String NEWLINE = "\n";
+
+ private static ImageIcon packetReceivedIcon;
+ private static ImageIcon packetSentIcon;
+ private static ImageIcon presencePacketIcon;
+ private static ImageIcon iqPacketIcon;
+ private static ImageIcon messagePacketIcon;
+ private static ImageIcon unknownPacketTypeIcon;
+
+ {
+ URL url;
+ // Load the image icons
+ url =
+ Thread.currentThread().getContextClassLoader().getResource("images/nav_left_blue.png");
+ if (url != null) {
+ packetReceivedIcon = new ImageIcon(url);
+ }
+ url =
+ Thread.currentThread().getContextClassLoader().getResource("images/nav_right_red.png");
+ if (url != null) {
+ packetSentIcon = new ImageIcon(url);
+ }
+ url =
+ Thread.currentThread().getContextClassLoader().getResource("images/photo_portrait.png");
+ if (url != null) {
+ presencePacketIcon = new ImageIcon(url);
+ }
+ url =
+ Thread.currentThread().getContextClassLoader().getResource(
+ "images/question_and_answer.png");
+ if (url != null) {
+ iqPacketIcon = new ImageIcon(url);
+ }
+ url = Thread.currentThread().getContextClassLoader().getResource("images/message.png");
+ if (url != null) {
+ messagePacketIcon = new ImageIcon(url);
+ }
+ url = Thread.currentThread().getContextClassLoader().getResource("images/unknown.png");
+ if (url != null) {
+ unknownPacketTypeIcon = new ImageIcon(url);
+ }
+ }
+
+ private DefaultTableModel messagesTable = null;
+ private JTextArea messageTextArea = null;
+ private JFormattedTextField userField = null;
+ private JFormattedTextField statusField = null;
+
+ private XMPPConnection connection = null;
+
+ private PacketListener packetReaderListener = null;
+ private PacketListener packetWriterListener = null;
+ private ConnectionListener connListener = null;
+
+ private Writer writer;
+ private Reader reader;
+ private ReaderListener readerListener;
+ private WriterListener writerListener;
+
+ private Date creationTime = new Date();
+
+ // Statistics variables
+ private DefaultTableModel statisticsTable = null;
+ private int sentPackets = 0;
+ private int receivedPackets = 0;
+ private int sentIQPackets = 0;
+ private int receivedIQPackets = 0;
+ private int sentMessagePackets = 0;
+ private int receivedMessagePackets = 0;
+ private int sentPresencePackets = 0;
+ private int receivedPresencePackets = 0;
+ private int sentOtherPackets = 0;
+ private int receivedOtherPackets = 0;
+
+ JTabbedPane tabbedPane;
+
+ public EnhancedDebugger(XMPPConnection connection, Writer writer, Reader reader) {
+ this.connection = connection;
+ this.writer = writer;
+ this.reader = reader;
+ createDebug();
+ EnhancedDebuggerWindow.addDebugger(this);
+ }
+
+ /**
+ * Creates the debug process, which is a GUI window that displays XML traffic.
+ */
+ private void createDebug() {
+ // We'll arrange the UI into six tabs. The first tab contains all data, the second
+ // client generated XML, the third server generated XML, the fourth allows to send
+ // ad-hoc messages and the fifth contains connection information.
+ tabbedPane = new JTabbedPane();
+
+ // Add the All Packets, Sent, Received and Interpreted panels
+ addBasicPanels();
+
+ // Add the panel to send ad-hoc messages
+ addAdhocPacketPanel();
+
+ // Add the connection information panel
+ addInformationPanel();
+
+ // Create a thread that will listen for all incoming packets and write them to
+ // the GUI. This is what we call "interpreted" packet data, since it's the packet
+ // data as Smack sees it and not as it's coming in as raw XML.
+ packetReaderListener = new PacketListener() {
+ SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa");
+
+ public void processPacket(final Packet packet) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ addReadPacketToTable(dateFormatter, packet);
+ }
+ });
+
+ }
+ };
+
+ // Create a thread that will listen for all outgoing packets and write them to
+ // the GUI.
+ packetWriterListener = new PacketListener() {
+ SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa");
+
+ public void processPacket(final Packet packet) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ addSentPacketToTable(dateFormatter, packet);
+ }
+ });
+
+ }
+ };
+
+ // Create a thread that will listen for any connection closed event
+ connListener = new ConnectionListener() {
+ public void connectionClosed() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ statusField.setValue("Closed");
+ EnhancedDebuggerWindow.connectionClosed(EnhancedDebugger.this);
+ }
+ });
+
+ }
+
+ public void connectionClosedOnError(final Exception e) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ statusField.setValue("Closed due to an exception");
+ EnhancedDebuggerWindow.connectionClosedOnError(EnhancedDebugger.this, e);
+ }
+ });
+
+ }
+ };
+ }
+
+ private void addBasicPanels() {
+ JSplitPane allPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+ allPane.setOneTouchExpandable(true);
+
+ messagesTable =
+ new DefaultTableModel(
+ new Object[]{"Hide", "Timestamp", "", "", "Message", "Id", "Type", "To", "From"},
+ 0) {
+ public boolean isCellEditable(int rowIndex, int mColIndex) {
+ return false;
+ }
+
+ public Class getColumnClass(int columnIndex) {
+ if (columnIndex == 2 || columnIndex == 3) {
+ return Icon.class;
+ }
+ return super.getColumnClass(columnIndex);
+ }
+
+ };
+ JTable table = new JTable(messagesTable);
+ // Allow only single a selection
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ // Hide the first column
+ table.getColumnModel().getColumn(0).setMaxWidth(0);
+ table.getColumnModel().getColumn(0).setMinWidth(0);
+ table.getTableHeader().getColumnModel().getColumn(0).setMaxWidth(0);
+ table.getTableHeader().getColumnModel().getColumn(0).setMinWidth(0);
+ // Set the column "timestamp" size
+ table.getColumnModel().getColumn(1).setMaxWidth(300);
+ table.getColumnModel().getColumn(1).setPreferredWidth(70);
+ // Set the column "direction" icon size
+ table.getColumnModel().getColumn(2).setMaxWidth(50);
+ table.getColumnModel().getColumn(2).setPreferredWidth(30);
+ // Set the column "packet type" icon size
+ table.getColumnModel().getColumn(3).setMaxWidth(50);
+ table.getColumnModel().getColumn(3).setPreferredWidth(30);
+ // Set the column "Id" size
+ table.getColumnModel().getColumn(5).setMaxWidth(100);
+ table.getColumnModel().getColumn(5).setPreferredWidth(55);
+ // Set the column "type" size
+ table.getColumnModel().getColumn(6).setMaxWidth(200);
+ table.getColumnModel().getColumn(6).setPreferredWidth(50);
+ // Set the column "to" size
+ table.getColumnModel().getColumn(7).setMaxWidth(300);
+ table.getColumnModel().getColumn(7).setPreferredWidth(90);
+ // Set the column "from" size
+ table.getColumnModel().getColumn(8).setMaxWidth(300);
+ table.getColumnModel().getColumn(8).setPreferredWidth(90);
+ // Create a table listener that listen for row selection events
+ SelectionListener selectionListener = new SelectionListener(table);
+ table.getSelectionModel().addListSelectionListener(selectionListener);
+ table.getColumnModel().getSelectionModel().addListSelectionListener(selectionListener);
+ allPane.setTopComponent(new JScrollPane(table));
+ messageTextArea = new JTextArea();
+ messageTextArea.setEditable(false);
+ // Add pop-up menu.
+ JPopupMenu menu = new JPopupMenu();
+ JMenuItem menuItem1 = new JMenuItem("Copy");
+ menuItem1.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ // Get the clipboard
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ // Set the sent text as the new content of the clipboard
+ clipboard.setContents(new StringSelection(messageTextArea.getText()), null);
+ }
+ });
+ menu.add(menuItem1);
+ // Add listener to the text area so the popup menu can come up.
+ messageTextArea.addMouseListener(new PopupListener(menu));
+ allPane.setBottomComponent(new JScrollPane(messageTextArea));
+ allPane.setDividerLocation(150);
+
+ tabbedPane.add("All Packets", allPane);
+ tabbedPane.setToolTipTextAt(0, "Sent and received packets processed by Smack");
+
+ // Create UI elements for client generated XML traffic.
+ final JTextArea sentText = new JTextArea();
+ sentText.setWrapStyleWord(true);
+ sentText.setLineWrap(true);
+ sentText.setEditable(false);
+ sentText.setForeground(new Color(112, 3, 3));
+ tabbedPane.add("Raw Sent Packets", new JScrollPane(sentText));
+ tabbedPane.setToolTipTextAt(1, "Raw text of the sent packets");
+
+ // Add pop-up menu.
+ menu = new JPopupMenu();
+ menuItem1 = new JMenuItem("Copy");
+ menuItem1.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ // Get the clipboard
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ // Set the sent text as the new content of the clipboard
+ clipboard.setContents(new StringSelection(sentText.getText()), null);
+ }
+ });
+
+ JMenuItem menuItem2 = new JMenuItem("Clear");
+ menuItem2.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ sentText.setText("");
+ }
+ });
+
+ // Add listener to the text area so the popup menu can come up.
+ sentText.addMouseListener(new PopupListener(menu));
+ menu.add(menuItem1);
+ menu.add(menuItem2);
+
+ // Create UI elements for server generated XML traffic.
+ final JTextArea receivedText = new JTextArea();
+ receivedText.setWrapStyleWord(true);
+ receivedText.setLineWrap(true);
+ receivedText.setEditable(false);
+ receivedText.setForeground(new Color(6, 76, 133));
+ tabbedPane.add("Raw Received Packets", new JScrollPane(receivedText));
+ tabbedPane.setToolTipTextAt(
+ 2,
+ "Raw text of the received packets before Smack process them");
+
+ // Add pop-up menu.
+ menu = new JPopupMenu();
+ menuItem1 = new JMenuItem("Copy");
+ menuItem1.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ // Get the clipboard
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ // Set the sent text as the new content of the clipboard
+ clipboard.setContents(new StringSelection(receivedText.getText()), null);
+ }
+ });
+
+ menuItem2 = new JMenuItem("Clear");
+ menuItem2.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ receivedText.setText("");
+ }
+ });
+
+ // Add listener to the text area so the popup menu can come up.
+ receivedText.addMouseListener(new PopupListener(menu));
+ menu.add(menuItem1);
+ menu.add(menuItem2);
+
+ // Create a special Reader that wraps the main Reader and logs data to the GUI.
+ ObservableReader debugReader = new ObservableReader(reader);
+ readerListener = new ReaderListener() {
+ public void read(final String str) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (EnhancedDebuggerWindow.PERSISTED_DEBUGGER &&
+ !EnhancedDebuggerWindow.getInstance().isVisible()) {
+ // Do not add content if the parent is not visible
+ return;
+ }
+
+ int index = str.lastIndexOf(">");
+ if (index != -1) {
+ if (receivedText.getLineCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS)
+ {
+ try {
+ receivedText.replaceRange("", 0, receivedText.getLineEndOffset(0));
+ }
+ catch (BadLocationException e) {
+ e.printStackTrace();
+ }
+ }
+ receivedText.append(str.substring(0, index + 1));
+ receivedText.append(NEWLINE);
+ if (str.length() > index) {
+ receivedText.append(str.substring(index + 1));
+ }
+ }
+ else {
+ receivedText.append(str);
+ }
+ }
+ });
+ }
+ };
+ debugReader.addReaderListener(readerListener);
+
+ // Create a special Writer that wraps the main Writer and logs data to the GUI.
+ ObservableWriter debugWriter = new ObservableWriter(writer);
+ writerListener = new WriterListener() {
+ public void write(final String str) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ if (EnhancedDebuggerWindow.PERSISTED_DEBUGGER &&
+ !EnhancedDebuggerWindow.getInstance().isVisible()) {
+ // Do not add content if the parent is not visible
+ return;
+ }
+
+ if (sentText.getLineCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) {
+ try {
+ sentText.replaceRange("", 0, sentText.getLineEndOffset(0));
+ }
+ catch (BadLocationException e) {
+ e.printStackTrace();
+ }
+ }
+
+ sentText.append(str);
+ if (str.endsWith(">")) {
+ sentText.append(NEWLINE);
+ }
+ }
+ });
+
+
+ }
+ };
+ debugWriter.addWriterListener(writerListener);
+
+ // Assign the reader/writer objects to use the debug versions. The packet reader
+ // and writer will use the debug versions when they are created.
+ reader = debugReader;
+ writer = debugWriter;
+
+ }
+
+ private void addAdhocPacketPanel() {
+ // Create UI elements for sending ad-hoc messages.
+ final JTextArea adhocMessages = new JTextArea();
+ adhocMessages.setEditable(true);
+ adhocMessages.setForeground(new Color(1, 94, 35));
+ tabbedPane.add("Ad-hoc message", new JScrollPane(adhocMessages));
+ tabbedPane.setToolTipTextAt(3, "Panel that allows you to send adhoc packets");
+
+ // Add pop-up menu.
+ JPopupMenu menu = new JPopupMenu();
+ JMenuItem menuItem = new JMenuItem("Message");
+ menuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ adhocMessages.setText(
+ "<message to=\"\" id=\""
+ + StringUtils.randomString(5)
+ + "-X\"><body></body></message>");
+ }
+ });
+ menu.add(menuItem);
+
+ menuItem = new JMenuItem("IQ Get");
+ menuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ adhocMessages.setText(
+ "<iq type=\"get\" to=\"\" id=\""
+ + StringUtils.randomString(5)
+ + "-X\"><query xmlns=\"\"></query></iq>");
+ }
+ });
+ menu.add(menuItem);
+
+ menuItem = new JMenuItem("IQ Set");
+ menuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ adhocMessages.setText(
+ "<iq type=\"set\" to=\"\" id=\""
+ + StringUtils.randomString(5)
+ + "-X\"><query xmlns=\"\"></query></iq>");
+ }
+ });
+ menu.add(menuItem);
+
+ menuItem = new JMenuItem("Presence");
+ menuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ adhocMessages.setText(
+ "<presence to=\"\" id=\"" + StringUtils.randomString(5) + "-X\"/>");
+ }
+ });
+ menu.add(menuItem);
+ menu.addSeparator();
+
+ menuItem = new JMenuItem("Send");
+ menuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ if (!"".equals(adhocMessages.getText())) {
+ AdHocPacket packetToSend = new AdHocPacket(adhocMessages.getText());
+ connection.sendPacket(packetToSend);
+ }
+ }
+ });
+ menu.add(menuItem);
+
+ menuItem = new JMenuItem("Clear");
+ menuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ adhocMessages.setText(null);
+ }
+ });
+ menu.add(menuItem);
+
+ // Add listener to the text area so the popup menu can come up.
+ adhocMessages.addMouseListener(new PopupListener(menu));
+ }
+
+ private void addInformationPanel() {
+ // Create UI elements for connection information.
+ JPanel informationPanel = new JPanel();
+ informationPanel.setLayout(new BorderLayout());
+
+ // Add the Host information
+ JPanel connPanel = new JPanel();
+ connPanel.setLayout(new GridBagLayout());
+ connPanel.setBorder(BorderFactory.createTitledBorder("Connection information"));
+
+ JLabel label = new JLabel("Host: ");
+ label.setMinimumSize(new java.awt.Dimension(150, 14));
+ label.setMaximumSize(new java.awt.Dimension(150, 14));
+ connPanel.add(
+ label,
+ new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0));
+ JFormattedTextField field = new JFormattedTextField(connection.getServiceName());
+ field.setMinimumSize(new java.awt.Dimension(150, 20));
+ field.setMaximumSize(new java.awt.Dimension(150, 20));
+ field.setEditable(false);
+ field.setBorder(null);
+ connPanel.add(
+ field,
+ new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0));
+
+ // Add the Port information
+ label = new JLabel("Port: ");
+ label.setMinimumSize(new java.awt.Dimension(150, 14));
+ label.setMaximumSize(new java.awt.Dimension(150, 14));
+ connPanel.add(
+ label,
+ new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0));
+ field = new JFormattedTextField(new Integer(connection.getPort()));
+ field.setMinimumSize(new java.awt.Dimension(150, 20));
+ field.setMaximumSize(new java.awt.Dimension(150, 20));
+ field.setEditable(false);
+ field.setBorder(null);
+ connPanel.add(
+ field,
+ new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0));
+
+ // Add the connection's User information
+ label = new JLabel("User: ");
+ label.setMinimumSize(new java.awt.Dimension(150, 14));
+ label.setMaximumSize(new java.awt.Dimension(150, 14));
+ connPanel.add(
+ label,
+ new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0));
+ userField = new JFormattedTextField();
+ userField.setMinimumSize(new java.awt.Dimension(150, 20));
+ userField.setMaximumSize(new java.awt.Dimension(150, 20));
+ userField.setEditable(false);
+ userField.setBorder(null);
+ connPanel.add(
+ userField,
+ new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0));
+
+ // Add the connection's creationTime information
+ label = new JLabel("Creation time: ");
+ label.setMinimumSize(new java.awt.Dimension(150, 14));
+ label.setMaximumSize(new java.awt.Dimension(150, 14));
+ connPanel.add(
+ label,
+ new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0));
+ field = new JFormattedTextField(new SimpleDateFormat("yyyy.MM.dd hh:mm:ss aaa"));
+ field.setMinimumSize(new java.awt.Dimension(150, 20));
+ field.setMaximumSize(new java.awt.Dimension(150, 20));
+ field.setValue(creationTime);
+ field.setEditable(false);
+ field.setBorder(null);
+ connPanel.add(
+ field,
+ new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0));
+
+ // Add the connection's creationTime information
+ label = new JLabel("Status: ");
+ label.setMinimumSize(new java.awt.Dimension(150, 14));
+ label.setMaximumSize(new java.awt.Dimension(150, 14));
+ connPanel.add(
+ label,
+ new GridBagConstraints(0, 4, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0));
+ statusField = new JFormattedTextField();
+ statusField.setMinimumSize(new java.awt.Dimension(150, 20));
+ statusField.setMaximumSize(new java.awt.Dimension(150, 20));
+ statusField.setValue("Active");
+ statusField.setEditable(false);
+ statusField.setBorder(null);
+ connPanel.add(
+ statusField,
+ new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0));
+ // Add the connection panel to the information panel
+ informationPanel.add(connPanel, BorderLayout.NORTH);
+
+ // Add the Number of sent packets information
+ JPanel packetsPanel = new JPanel();
+ packetsPanel.setLayout(new GridLayout(1, 1));
+ packetsPanel.setBorder(BorderFactory.createTitledBorder("Transmitted Packets"));
+
+ statisticsTable =
+ new DefaultTableModel(new Object[][]{{"IQ", new Integer(0), new Integer(0)}, {
+ "Message", new Integer(0), new Integer(0)
+ }, {
+ "Presence", new Integer(0), new Integer(0)
+ }, {
+ "Other", new Integer(0), new Integer(0)
+ }, {
+ "Total", new Integer(0), new Integer(0)
+ }
+ }, new Object[]{"Type", "Received", "Sent"}) {
+ public boolean isCellEditable(int rowIndex, int mColIndex) {
+ return false;
+ }
+ };
+ JTable table = new JTable(statisticsTable);
+ // Allow only single a selection
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ packetsPanel.add(new JScrollPane(table));
+
+ // Add the packets panel to the information panel
+ informationPanel.add(packetsPanel, BorderLayout.CENTER);
+
+ tabbedPane.add("Information", new JScrollPane(informationPanel));
+ tabbedPane.setToolTipTextAt(4, "Information and statistics about the debugged connection");
+ }
+
+ public Reader newConnectionReader(Reader newReader) {
+ ((ObservableReader) reader).removeReaderListener(readerListener);
+ ObservableReader debugReader = new ObservableReader(newReader);
+ debugReader.addReaderListener(readerListener);
+ reader = debugReader;
+ return reader;
+ }
+
+ public Writer newConnectionWriter(Writer newWriter) {
+ ((ObservableWriter) writer).removeWriterListener(writerListener);
+ ObservableWriter debugWriter = new ObservableWriter(newWriter);
+ debugWriter.addWriterListener(writerListener);
+ writer = debugWriter;
+ return writer;
+ }
+
+ public void userHasLogged(final String user) {
+ final EnhancedDebugger debugger = this;
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ userField.setText(user);
+ EnhancedDebuggerWindow.userHasLogged(debugger, user);
+ // Add the connection listener to the connection so that the debugger can be notified
+ // whenever the connection is closed.
+ connection.addConnectionListener(connListener);
+ }
+ });
+
+ }
+
+ public Reader getReader() {
+ return reader;
+ }
+
+ public Writer getWriter() {
+ return writer;
+ }
+
+ public PacketListener getReaderListener() {
+ return packetReaderListener;
+ }
+
+ public PacketListener getWriterListener() {
+ return packetWriterListener;
+ }
+
+ /**
+ * Updates the statistics table
+ */
+ private void updateStatistics() {
+ statisticsTable.setValueAt(new Integer(receivedIQPackets), 0, 1);
+ statisticsTable.setValueAt(new Integer(sentIQPackets), 0, 2);
+
+ statisticsTable.setValueAt(new Integer(receivedMessagePackets), 1, 1);
+ statisticsTable.setValueAt(new Integer(sentMessagePackets), 1, 2);
+
+ statisticsTable.setValueAt(new Integer(receivedPresencePackets), 2, 1);
+ statisticsTable.setValueAt(new Integer(sentPresencePackets), 2, 2);
+
+ statisticsTable.setValueAt(new Integer(receivedOtherPackets), 3, 1);
+ statisticsTable.setValueAt(new Integer(sentOtherPackets), 3, 2);
+
+ statisticsTable.setValueAt(new Integer(receivedPackets), 4, 1);
+ statisticsTable.setValueAt(new Integer(sentPackets), 4, 2);
+ }
+
+ /**
+ * Adds the received packet detail to the messages table.
+ *
+ * @param dateFormatter the SimpleDateFormat to use to format Dates
+ * @param packet the read packet to add to the table
+ */
+ private void addReadPacketToTable(final SimpleDateFormat dateFormatter, final Packet packet) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ String messageType = null;
+ String from = packet.getFrom();
+ String type = "";
+ Icon packetTypeIcon;
+ receivedPackets++;
+ if (packet instanceof IQ) {
+ packetTypeIcon = iqPacketIcon;
+ messageType = "IQ Received (class=" + packet.getClass().getName() + ")";
+ type = ((IQ) packet).getType().toString();
+ receivedIQPackets++;
+ }
+ else if (packet instanceof Message) {
+ packetTypeIcon = messagePacketIcon;
+ messageType = "Message Received";
+ type = ((Message) packet).getType().toString();
+ receivedMessagePackets++;
+ }
+ else if (packet instanceof Presence) {
+ packetTypeIcon = presencePacketIcon;
+ messageType = "Presence Received";
+ type = ((Presence) packet).getType().toString();
+ receivedPresencePackets++;
+ }
+ else {
+ packetTypeIcon = unknownPacketTypeIcon;
+ messageType = packet.getClass().getName() + " Received";
+ receivedOtherPackets++;
+ }
+
+ // Check if we need to remove old rows from the table to keep memory consumption low
+ if (EnhancedDebuggerWindow.MAX_TABLE_ROWS > 0 &&
+ messagesTable.getRowCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) {
+ messagesTable.removeRow(0);
+ }
+
+ messagesTable.addRow(
+ new Object[]{
+ formatXML(packet.toXML()),
+ dateFormatter.format(new Date()),
+ packetReceivedIcon,
+ packetTypeIcon,
+ messageType,
+ packet.getPacketID(),
+ type,
+ "",
+ from});
+ // Update the statistics table
+ updateStatistics();
+ }
+ });
+ }
+
+ /**
+ * Adds the sent packet detail to the messages table.
+ *
+ * @param dateFormatter the SimpleDateFormat to use to format Dates
+ * @param packet the sent packet to add to the table
+ */
+ private void addSentPacketToTable(final SimpleDateFormat dateFormatter, final Packet packet) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ String messageType = null;
+ String to = packet.getTo();
+ String type = "";
+ Icon packetTypeIcon;
+ sentPackets++;
+ if (packet instanceof IQ) {
+ packetTypeIcon = iqPacketIcon;
+ messageType = "IQ Sent (class=" + packet.getClass().getName() + ")";
+ type = ((IQ) packet).getType().toString();
+ sentIQPackets++;
+ }
+ else if (packet instanceof Message) {
+ packetTypeIcon = messagePacketIcon;
+ messageType = "Message Sent";
+ type = ((Message) packet).getType().toString();
+ sentMessagePackets++;
+ }
+ else if (packet instanceof Presence) {
+ packetTypeIcon = presencePacketIcon;
+ messageType = "Presence Sent";
+ type = ((Presence) packet).getType().toString();
+ sentPresencePackets++;
+ }
+ else {
+ packetTypeIcon = unknownPacketTypeIcon;
+ messageType = packet.getClass().getName() + " Sent";
+ sentOtherPackets++;
+ }
+
+ // Check if we need to remove old rows from the table to keep memory consumption low
+ if (EnhancedDebuggerWindow.MAX_TABLE_ROWS > 0 &&
+ messagesTable.getRowCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) {
+ messagesTable.removeRow(0);
+ }
+
+ messagesTable.addRow(
+ new Object[]{
+ formatXML(packet.toXML()),
+ dateFormatter.format(new Date()),
+ packetSentIcon,
+ packetTypeIcon,
+ messageType,
+ packet.getPacketID(),
+ type,
+ to,
+ ""});
+
+ // Update the statistics table
+ updateStatistics();
+ }
+ });
+ }
+
+ private String formatXML(String str) {
+ try {
+ // Use a Transformer for output
+ TransformerFactory tFactory = TransformerFactory.newInstance();
+ // Surround this setting in a try/catch for compatibility with Java 1.4. This setting is required
+ // for Java 1.5
+ try {
+ tFactory.setAttribute("indent-number", new Integer(2));
+ }
+ catch (IllegalArgumentException e) {
+ }
+ Transformer transformer = tFactory.newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
+
+ // Transform the requested string into a nice formatted XML string
+ StreamSource source = new StreamSource(new StringReader(str));
+ StringWriter sw = new StringWriter();
+ StreamResult result = new StreamResult(sw);
+ transformer.transform(source, result);
+ return sw.toString();
+
+ }
+ catch (TransformerConfigurationException tce) {
+ // Error generated by the parser
+ System.out.println("\n** Transformer Factory error");
+ System.out.println(" " + tce.getMessage());
+
+ // Use the contained exception, if any
+ Throwable x = tce;
+ if (tce.getException() != null)
+ x = tce.getException();
+ x.printStackTrace();
+
+ }
+ catch (TransformerException te) {
+ // Error generated by the parser
+ System.out.println("\n** Transformation error");
+ System.out.println(" " + te.getMessage());
+
+ // Use the contained exception, if any
+ Throwable x = te;
+ if (te.getException() != null)
+ x = te.getException();
+ x.printStackTrace();
+
+ }
+ return str;
+ }
+
+ /**
+ * Returns true if the debugger's connection with the server is up and running.
+ *
+ * @return true if the connection with the server is active.
+ */
+ boolean isConnectionActive() {
+ return connection.isConnected();
+ }
+
+ /**
+ * Stops debugging the connection. Removes any listener on the connection.
+ */
+ void cancel() {
+ connection.removeConnectionListener(connListener);
+ connection.removePacketListener(packetReaderListener);
+ connection.removePacketWriterListener(packetWriterListener);
+ ((ObservableReader) reader).removeReaderListener(readerListener);
+ ((ObservableWriter) writer).removeWriterListener(writerListener);
+ messagesTable = null;
+ }
+
+ /**
+ * An ad-hoc packet is like any regular packet but with the exception that it's intention is
+ * to be used only <b>to send packets</b>.<p>
+ * <p/>
+ * The whole text to send must be passed to the constructor. This implies that the client of
+ * this class is responsible for sending a valid text to the constructor.
+ */
+ private class AdHocPacket extends Packet {
+
+ private String text;
+
+ /**
+ * Create a new AdHocPacket with the text to send. The passed text must be a valid text to
+ * send to the server, no validation will be done on the passed text.
+ *
+ * @param text the whole text of the packet to send
+ */
+ public AdHocPacket(String text) {
+ this.text = text;
+ }
+
+ public String toXML() {
+ return text;
+ }
+
+ }
+
+ /**
+ * Listens for debug window popup dialog events.
+ */
+ private class PopupListener extends MouseAdapter {
+
+ JPopupMenu popup;
+
+ PopupListener(JPopupMenu popupMenu) {
+ popup = popupMenu;
+ }
+
+ public void mousePressed(MouseEvent e) {
+ maybeShowPopup(e);
+ }
+
+ public void mouseReleased(MouseEvent e) {
+ maybeShowPopup(e);
+ }
+
+ private void maybeShowPopup(MouseEvent e) {
+ if (e.isPopupTrigger()) {
+ popup.show(e.getComponent(), e.getX(), e.getY());
+ }
+ }
+ }
+
+ private class SelectionListener implements ListSelectionListener {
+
+ JTable table;
+
+ // It is necessary to keep the table since it is not possible
+ // to determine the table from the event's source
+ SelectionListener(JTable table) {
+ this.table = table;
+ }
+
+ public void valueChanged(ListSelectionEvent e) {
+ if (table.getSelectedRow() == -1) {
+ // Clear the messageTextArea since there is none packet selected
+ messageTextArea.setText(null);
+ }
+ else {
+ // Set the detail of the packet in the messageTextArea
+ messageTextArea.setText(
+ (String) table.getModel().getValueAt(table.getSelectedRow(), 0));
+ // Scroll up to the top
+ messageTextArea.setCaretPosition(0);
+ }
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java
new file mode 100644
index 000000000..d47a062f0
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java
@@ -0,0 +1,377 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.debugger;
+
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.provider.ProviderManager;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.*;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Vector;
+
+/**
+ * The EnhancedDebuggerWindow is the main debug window that will show all the EnhancedDebuggers.
+ * For each connection to debug there will be an EnhancedDebugger that will be shown in the
+ * EnhancedDebuggerWindow.<p>
+ * <p/>
+ * This class also provides information about Smack like for example the Smack version and the
+ * installed providers.
+ *
+ * @author Gaston Dombiak
+ */
+public class EnhancedDebuggerWindow {
+
+ private static EnhancedDebuggerWindow instance;
+
+ private static ImageIcon connectionCreatedIcon;
+ private static ImageIcon connectionActiveIcon;
+ private static ImageIcon connectionClosedIcon;
+ private static ImageIcon connectionClosedOnErrorIcon;
+
+ public static boolean PERSISTED_DEBUGGER = false;
+ /**
+ * Keeps the max number of rows to keep in the tables. A value less than 0 means that packets
+ * will never be removed. If you are planning to use this debugger in a
+ * production environment then you should set a lower value (e.g. 50) to prevent the debugger
+ * from consuming all the JVM memory.
+ */
+ public static int MAX_TABLE_ROWS = 150;
+
+ {
+ URL url;
+
+ url =
+ Thread.currentThread().getContextClassLoader().getResource(
+ "images/trafficlight_off.png");
+ if (url != null) {
+ connectionCreatedIcon = new ImageIcon(url);
+ }
+ url =
+ Thread.currentThread().getContextClassLoader().getResource(
+ "images/trafficlight_green.png");
+ if (url != null) {
+ connectionActiveIcon = new ImageIcon(url);
+ }
+ url =
+ Thread.currentThread().getContextClassLoader().getResource(
+ "images/trafficlight_red.png");
+ if (url != null) {
+ connectionClosedIcon = new ImageIcon(url);
+ }
+ url = Thread.currentThread().getContextClassLoader().getResource("images/warning.png");
+ if (url != null) {
+ connectionClosedOnErrorIcon = new ImageIcon(url);
+ }
+
+ }
+
+ private JFrame frame = null;
+ private JTabbedPane tabbedPane = null;
+ private java.util.List debuggers = new ArrayList();
+
+ private EnhancedDebuggerWindow() {
+ }
+
+ /**
+ * Returns the unique EnhancedDebuggerWindow instance available in the system.
+ *
+ * @return the unique EnhancedDebuggerWindow instance
+ */
+ public static EnhancedDebuggerWindow getInstance() {
+ if (instance == null) {
+ instance = new EnhancedDebuggerWindow();
+ }
+ return instance;
+ }
+
+ /**
+ * Adds the new specified debugger to the list of debuggers to show in the main window.
+ *
+ * @param debugger the new debugger to show in the debug window
+ */
+ synchronized static void addDebugger(EnhancedDebugger debugger) {
+ getInstance().showNewDebugger(debugger);
+ }
+
+ /**
+ * Shows the new debugger in the debug window.
+ *
+ * @param debugger the new debugger to show
+ */
+ private void showNewDebugger(EnhancedDebugger debugger) {
+ if (frame == null) {
+ createDebug();
+ }
+ debugger.tabbedPane.setName("Connection_" + tabbedPane.getComponentCount());
+ tabbedPane.add(debugger.tabbedPane, tabbedPane.getComponentCount() - 1);
+ tabbedPane.setIconAt(tabbedPane.indexOfComponent(debugger.tabbedPane), connectionCreatedIcon);
+ frame.setTitle(
+ "Smack Debug Window -- Total connections: " + (tabbedPane.getComponentCount() - 1));
+ // Keep the added debugger for later access
+ debuggers.add(debugger);
+ }
+
+ /**
+ * Notification that a user has logged in to the server. A new title will be set
+ * to the tab of the given debugger.
+ *
+ * @param debugger the debugger whose connection logged in to the server
+ * @param user the user@host/resource that has just logged in
+ */
+ synchronized static void userHasLogged(EnhancedDebugger debugger, String user) {
+ int index = getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane);
+ getInstance().tabbedPane.setTitleAt(
+ index,
+ user);
+ getInstance().tabbedPane.setIconAt(
+ index,
+ connectionActiveIcon);
+ }
+
+ /**
+ * Notification that the connection was properly closed.
+ *
+ * @param debugger the debugger whose connection was properly closed.
+ */
+ synchronized static void connectionClosed(EnhancedDebugger debugger) {
+ getInstance().tabbedPane.setIconAt(
+ getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane),
+ connectionClosedIcon);
+ }
+
+ /**
+ * Notification that the connection was closed due to an exception.
+ *
+ * @param debugger the debugger whose connection was closed due to an exception.
+ * @param e the exception.
+ */
+ synchronized static void connectionClosedOnError(EnhancedDebugger debugger, Exception e) {
+ int index = getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane);
+ getInstance().tabbedPane.setToolTipTextAt(
+ index,
+ "Connection closed due to the exception: " + e.getMessage());
+ getInstance().tabbedPane.setIconAt(
+ index,
+ connectionClosedOnErrorIcon);
+ }
+
+ /**
+ * Creates the main debug window that provides information about Smack and also shows
+ * a tab panel for each connection that is being debugged.
+ */
+ private void createDebug() {
+
+ frame = new JFrame("Smack Debug Window");
+
+ if (!PERSISTED_DEBUGGER) {
+ // Add listener for window closing event
+ frame.addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent evt) {
+ rootWindowClosing(evt);
+ }
+ });
+ }
+
+ // We'll arrange the UI into tabs. The last tab contains Smack's information.
+ // All the connection debugger tabs will be shown before the Smack info tab.
+ tabbedPane = new JTabbedPane();
+
+ // Create the Smack info panel
+ JPanel informationPanel = new JPanel();
+ informationPanel.setLayout(new BoxLayout(informationPanel, BoxLayout.Y_AXIS));
+
+ // Add the Smack version label
+ JPanel versionPanel = new JPanel();
+ versionPanel.setLayout(new BoxLayout(versionPanel, BoxLayout.X_AXIS));
+ versionPanel.setMaximumSize(new Dimension(2000, 31));
+ versionPanel.add(new JLabel(" Smack version: "));
+ JFormattedTextField field = new JFormattedTextField(SmackConfiguration.getVersion());
+ field.setEditable(false);
+ field.setBorder(null);
+ versionPanel.add(field);
+ informationPanel.add(versionPanel);
+
+ // Add the list of installed IQ Providers
+ JPanel iqProvidersPanel = new JPanel();
+ iqProvidersPanel.setLayout(new GridLayout(1, 1));
+ iqProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed IQ Providers"));
+ Vector providers = new Vector();
+ for (Iterator it = ProviderManager.getDefault().getIQProviders(); it.hasNext();) {
+ Object provider = it.next();
+ if (provider.getClass() == Class.class) {
+ providers.add(((Class) provider).getName());
+ }
+ else {
+ providers.add(provider.getClass().getName());
+ }
+ }
+ // Sort the collection of providers
+ Collections.sort(providers);
+ JList list = new JList(providers);
+ iqProvidersPanel.add(new JScrollPane(list));
+ informationPanel.add(iqProvidersPanel);
+
+ // Add the list of installed Extension Providers
+ JPanel extensionProvidersPanel = new JPanel();
+ extensionProvidersPanel.setLayout(new GridLayout(1, 1));
+ extensionProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed Extension Providers"));
+ providers = new Vector();
+ for (Iterator it = ProviderManager.getDefault().getExtensionProviders(); it.hasNext();) {
+ Object provider = it.next();
+ if (provider.getClass() == Class.class) {
+ providers.add(((Class) provider).getName());
+ }
+ else {
+ providers.add(provider.getClass().getName());
+ }
+ }
+ // Sort the collection of providers
+ Collections.sort(providers);
+ list = new JList(providers);
+ extensionProvidersPanel.add(new JScrollPane(list));
+ informationPanel.add(extensionProvidersPanel);
+
+ tabbedPane.add("Smack Info", informationPanel);
+
+ // Add pop-up menu.
+ JPopupMenu menu = new JPopupMenu();
+ // Add a menu item that allows to close the current selected tab
+ JMenuItem menuItem = new JMenuItem("Close");
+ menuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ // Remove the selected tab pane if it's not the Smack info pane
+ if (tabbedPane.getSelectedIndex() < tabbedPane.getComponentCount() - 1) {
+ int index = tabbedPane.getSelectedIndex();
+ // Notify to the debugger to stop debugging
+ EnhancedDebugger debugger = (EnhancedDebugger) debuggers.get(index);
+ debugger.cancel();
+ // Remove the debugger from the root window
+ tabbedPane.remove(debugger.tabbedPane);
+ debuggers.remove(debugger);
+ // Update the root window title
+ frame.setTitle(
+ "Smack Debug Window -- Total connections: "
+ + (tabbedPane.getComponentCount() - 1));
+ }
+ }
+ });
+ menu.add(menuItem);
+ // Add a menu item that allows to close all the tabs that have their connections closed
+ menuItem = new JMenuItem("Close All Not Active");
+ menuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ ArrayList debuggersToRemove = new ArrayList();
+ // Remove all the debuggers of which their connections are no longer valid
+ for (int index = 0; index < tabbedPane.getComponentCount() - 1; index++) {
+ EnhancedDebugger debugger = (EnhancedDebugger) debuggers.get(index);
+ if (!debugger.isConnectionActive()) {
+ // Notify to the debugger to stop debugging
+ debugger.cancel();
+ debuggersToRemove.add(debugger);
+ }
+ }
+ for (Iterator it = debuggersToRemove.iterator(); it.hasNext();) {
+ EnhancedDebugger debugger = (EnhancedDebugger) it.next();
+ // Remove the debugger from the root window
+ tabbedPane.remove(debugger.tabbedPane);
+ debuggers.remove(debugger);
+ }
+ // Update the root window title
+ frame.setTitle(
+ "Smack Debug Window -- Total connections: "
+ + (tabbedPane.getComponentCount() - 1));
+ }
+ });
+ menu.add(menuItem);
+ // Add listener to the text area so the popup menu can come up.
+ tabbedPane.addMouseListener(new PopupListener(menu));
+
+ frame.getContentPane().add(tabbedPane);
+
+ frame.setSize(650, 400);
+
+ if (!PERSISTED_DEBUGGER) {
+ frame.setVisible(true);
+ }
+ }
+
+ /**
+ * Notification that the root window is closing. Stop listening for received and
+ * transmitted packets in all the debugged connections.
+ *
+ * @param evt the event that indicates that the root window is closing
+ */
+ public void rootWindowClosing(WindowEvent evt) {
+ // Notify to all the debuggers to stop debugging
+ for (Iterator it = debuggers.iterator(); it.hasNext();) {
+ EnhancedDebugger debugger = (EnhancedDebugger) it.next();
+ debugger.cancel();
+ }
+ // Release any reference to the debuggers
+ debuggers.removeAll(debuggers);
+ // Release the default instance
+ instance = null;
+ }
+
+ /**
+ * Listens for debug window popup dialog events.
+ */
+ private class PopupListener extends MouseAdapter {
+
+ JPopupMenu popup;
+
+ PopupListener(JPopupMenu popupMenu) {
+ popup = popupMenu;
+ }
+
+ public void mousePressed(MouseEvent e) {
+ maybeShowPopup(e);
+ }
+
+ public void mouseReleased(MouseEvent e) {
+ maybeShowPopup(e);
+ }
+
+ private void maybeShowPopup(MouseEvent e) {
+ if (e.isPopupTrigger()) {
+ popup.show(e.getComponent(), e.getX(), e.getY());
+ }
+ }
+ }
+
+ public void setVisible(boolean visible) {
+ if (frame != null) {
+ frame.setVisible(visible);
+ }
+ }
+
+ public boolean isVisible() {
+ if (frame != null) {
+ return frame.isVisible();
+ }
+ return false;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/package.html
new file mode 100644
index 000000000..8ea20e0ac
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/package.html
@@ -0,0 +1 @@
+<body>Smack optional Debuggers.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Base64.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Base64.java
new file mode 100644
index 000000000..8a7cf59a4
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Base64.java
@@ -0,0 +1,1416 @@
+package org.jivesoftware.smackx.filetransfer;
+
+/**
+ * Encodes and decodes to and from Base64 notation. <p/> <p/> Change Log:
+ * </p>
+ * <ul>
+ * <li>v2.1 - Cleaned up javadoc comments and unused variables and methods.
+ * Added some convenience methods for reading and writing to and from files.</li>
+ * <li>v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on
+ * systems with other encodings (like EBCDIC).</li>
+ * <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the
+ * encoded data was a single byte.</li>
+ * <li>v2.0 - I got rid of methods that used booleans to set options. Now
+ * everything is more consolidated and cleaner. The code now detects when data
+ * that's being decoded is gzip-compressed and will decompress it automatically.
+ * Generally things are cleaner. You'll probably have to change some method
+ * calls that you were making to support the new options format (<tt>int</tt>s
+ * that you "OR" together).</li>
+ * <li>v1.5.1 - Fixed bug when decompressing and decoding to a byte[] using
+ * <tt>decode( String s, boolean gzipCompressed )</tt>. Added the ability to
+ * "suspend" encoding in the Output Stream so you can turn on and off the
+ * encoding if you need to embed base64 data in an otherwise "normal" stream
+ * (like an XML file).</li>
+ * <li>v1.5 - Output stream pases on flush() command but doesn't do anything
+ * itself. This helps when using GZIP streams. Added the ability to
+ * GZip-compress objects before encoding them.</li>
+ * <li>v1.4 - Added helper methods to read/write files.</li>
+ * <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li>
+ * <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input
+ * stream where last buffer being read, if not completely full, was not
+ * returned.</li>
+ * <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the
+ * wrong time.</li>
+ * <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li>
+ * </ul>
+ * <p/> <p/> I am placing this code in the Public Domain. Do with it as you
+ * will. This software comes with no guarantees or warranties but with plenty of
+ * well-wishing instead! Please visit <a
+ * href="http://iharder.net/base64">http://iharder.net/base64</a> periodically
+ * to check for updates or to contribute improvements.
+ * </p>
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.1
+ */
+class Base64 {
+
+ /* ******** P U B L I C F I E L D S ******** */
+
+ /**
+ * No options specified. Value is zero.
+ */
+ public final static int NO_OPTIONS = 0;
+
+ /**
+ * Specify encoding.
+ */
+ public final static int ENCODE = 1;
+
+ /**
+ * Specify decoding.
+ */
+ public final static int DECODE = 0;
+
+ /**
+ * Specify that data should be gzip-compressed.
+ */
+ public final static int GZIP = 2;
+
+ /**
+ * Don't break lines when encoding (violates strict Base64 specification)
+ */
+ public final static int DONT_BREAK_LINES = 8;
+
+ /* ******** P R I V A T E F I E L D S ******** */
+
+ /**
+ * Maximum line length (76) of Base64 output.
+ */
+ private final static int MAX_LINE_LENGTH = 76;
+
+ /**
+ * The equals sign (=) as a byte.
+ */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /**
+ * The new line character (\n) as a byte.
+ */
+ private final static byte NEW_LINE = (byte) '\n';
+
+ /**
+ * Preferred encoding.
+ */
+ private final static String PREFERRED_ENCODING = "UTF-8";
+
+ /**
+ * The 64 valid Base64 values.
+ */
+ private final static byte[] ALPHABET;
+
+ private final static byte[] _NATIVE_ALPHABET = /*
+ * May be something funny
+ * like EBCDIC
+ */
+ { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+ (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+ (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+ (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+ (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+ (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+ (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+ (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+ (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+ (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+ (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+ (byte) '9', (byte) '+', (byte) '/' };
+
+ /** Determine which ALPHABET to use. */
+ static {
+ byte[] __bytes;
+ try {
+ __bytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+ .getBytes(PREFERRED_ENCODING);
+ } // end try
+ catch (java.io.UnsupportedEncodingException use) {
+ __bytes = _NATIVE_ALPHABET; // Fall back to native encoding
+ } // end catch
+ ALPHABET = __bytes;
+ } // end static
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value or a
+ * negative number indicating some other meaning.
+ */
+ private final static byte[] DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9,
+ -9, // Decimal 0 - 8
+ -5, -5, // Whitespace: Tab and Linefeed
+ -9, -9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 -
+ // 26
+ -9, -9, -9, -9, -9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9, -9, -9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+ -9, -9, -9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9, -9, -9, // Decimal 62 - 64
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A'
+ // through 'N'
+ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O'
+ // through 'Z'
+ -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+ 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a'
+ // through 'm'
+ 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n'
+ // through 'z'
+ -9, -9, -9, -9 // Decimal 123 - 126
+ /*
+ * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ */
+ };
+
+ // I think I end up not using the BAD_ENCODING indicator.
+ // private final static byte BAD_ENCODING = -9; // Indicates error in
+ // encoding
+ private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in
+ // encoding
+
+ private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in
+ // encoding
+
+ /**
+ * Defeats instantiation.
+ */
+ private Base64() {
+ }
+
+ /* ******** E N C O D I N G M E T H O D S ******** */
+
+ /**
+ * Encodes up to the first three bytes of array <var>threeBytes</var> and
+ * returns a four-byte array in Base64 notation. The actual number of
+ * significant bytes in your array is given by <var>numSigBytes</var>. The
+ * array <var>threeBytes</var> needs only be as big as <var>numSigBytes</var>.
+ * Code can reuse a byte array by passing a four-byte array as <var>b4</var>.
+ *
+ * @param b4
+ * A reusable byte array to reduce array instantiation
+ * @param threeBytes
+ * the array to convert
+ * @param numSigBytes
+ * the number of significant bytes in your array
+ * @return four byte array in Base64 notation.
+ * @since 1.5.1
+ */
+ private static byte[] encode3to4(byte[] b4, byte[] threeBytes,
+ int numSigBytes) {
+ encode3to4(threeBytes, 0, numSigBytes, b4, 0);
+ return b4;
+ } // end encode3to4
+
+ /**
+ * Encodes up to three bytes of the array <var>source</var> and writes the
+ * resulting four Base64 bytes to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
+ * does not check to make sure your arrays are large enough to accomodate
+ * <var>srcOffset</var> + 3 for the <var>source</var> array or
+ * <var>destOffset</var> + 4 for the <var>destination</var> array. The
+ * actual number of significant bytes in your array is given by
+ * <var>numSigBytes</var>.
+ *
+ * @param source
+ * the array to convert
+ * @param srcOffset
+ * the index where conversion begins
+ * @param numSigBytes
+ * the number of significant bytes in your array
+ * @param destination
+ * the array to hold the conversion
+ * @param destOffset
+ * the index where output will be put
+ * @return the <var>destination</var> array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset,
+ int numSigBytes, byte[] destination, int destOffset) {
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index ALPHABET
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an
+ // int.
+ int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f];
+ return destination;
+
+ case 2:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+
+ case 1:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+ /**
+ * Serializes an object and returns the Base64-encoded version of that
+ * serialized object. If the object cannot be serialized or there is another
+ * error, the method will return <tt>null</tt>. The object is not
+ * GZip-compressed before being encoded.
+ *
+ * @param serializableObject
+ * The object to encode
+ * @return The Base64-encoded object
+ * @since 1.4
+ */
+ public static String encodeObject(java.io.Serializable serializableObject) {
+ return encodeObject(serializableObject, NO_OPTIONS);
+ } // end encodeObject
+
+ /**
+ * Serializes an object and returns the Base64-encoded version of that
+ * serialized object. If the object cannot be serialized or there is another
+ * error, the method will return <tt>null</tt>. <p/> Valid options:
+ *
+ * <pre>
+ * GZIP: gzip-compresses object before encoding it.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * &lt;i&gt;Note: Technically, this makes your encoding non-compliant.&lt;/i&gt;
+ * </pre>
+ *
+ * <p/> Example: <code>encodeObject( myObj, Base64.GZIP )</code> or <p/>
+ * Example:
+ * <code>encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+ *
+ * @param serializableObject
+ * The object to encode
+ * @param options
+ * Specified options
+ * @return The Base64-encoded object
+ * @see Base64#GZIP
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public static String encodeObject(java.io.Serializable serializableObject,
+ int options) {
+ // Streams
+ java.io.ByteArrayOutputStream baos = null;
+ java.io.OutputStream b64os = null;
+ java.io.ObjectOutputStream oos = null;
+ java.util.zip.GZIPOutputStream gzos = null;
+
+ // Isolate options
+ int gzip = (options & GZIP);
+ int dontBreakLines = (options & DONT_BREAK_LINES);
+
+ try {
+ // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream
+ baos = new java.io.ByteArrayOutputStream();
+ b64os = new Base64.OutputStream(baos, ENCODE | dontBreakLines);
+
+ // GZip?
+ if (gzip == GZIP) {
+ gzos = new java.util.zip.GZIPOutputStream(b64os);
+ oos = new java.io.ObjectOutputStream(gzos);
+ } // end if: gzip
+ else {
+ oos = new java.io.ObjectOutputStream(b64os);
+ }
+
+ oos.writeObject(serializableObject);
+ } // end try
+ catch (java.io.IOException e) {
+ e.printStackTrace();
+ return null;
+ } // end catch
+ finally {
+ try {
+ oos.close();
+ } catch (Exception e) {
+ }
+ try {
+ gzos.close();
+ } catch (Exception e) {
+ }
+ try {
+ b64os.close();
+ } catch (Exception e) {
+ }
+ try {
+ baos.close();
+ } catch (Exception e) {
+ }
+ } // end finally
+
+ // Return value according to relevant encoding.
+ try {
+ return new String(baos.toByteArray(), PREFERRED_ENCODING);
+ } // end try
+ catch (java.io.UnsupportedEncodingException uue) {
+ return new String(baos.toByteArray());
+ } // end catch
+
+ } // end encode
+
+ /**
+ * Encodes a byte array into Base64 notation. Does not GZip-compress data.
+ *
+ * @param source
+ * The data to convert
+ * @since 1.4
+ */
+ public static String encodeBytes(byte[] source) {
+ return encodeBytes(source, 0, source.length, NO_OPTIONS);
+ } // end encodeBytes
+
+ /**
+ * Encodes a byte array into Base64 notation. <p/> Valid options:
+ *
+ * <pre>
+ * GZIP: gzip-compresses object before encoding it.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * &lt;i&gt;Note: Technically, this makes your encoding non-compliant.&lt;/i&gt;
+ * </pre>
+ *
+ * <p/> Example: <code>encodeBytes( myData, Base64.GZIP )</code> or <p/>
+ * Example:
+ * <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+ *
+ * @param source
+ * The data to convert
+ * @param options
+ * Specified options
+ * @see Base64#GZIP
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public static String encodeBytes(byte[] source, int options) {
+ return encodeBytes(source, 0, source.length, options);
+ } // end encodeBytes
+
+ /**
+ * Encodes a byte array into Base64 notation. Does not GZip-compress data.
+ *
+ * @param source
+ * The data to convert
+ * @param off
+ * Offset in array where conversion should begin
+ * @param len
+ * Length of data to convert
+ * @since 1.4
+ */
+ public static String encodeBytes(byte[] source, int off, int len) {
+ return encodeBytes(source, off, len, NO_OPTIONS);
+ } // end encodeBytes
+
+ /**
+ * Encodes a byte array into Base64 notation. <p/> Valid options:
+ *
+ * <pre>
+ * GZIP: gzip-compresses object before encoding it.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * &lt;i&gt;Note: Technically, this makes your encoding non-compliant.&lt;/i&gt;
+ * </pre>
+ *
+ * <p/> Example: <code>encodeBytes( myData, Base64.GZIP )</code> or <p/>
+ * Example:
+ * <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+ *
+ * @param source
+ * The data to convert
+ * @param off
+ * Offset in array where conversion should begin
+ * @param len
+ * Length of data to convert
+ * @param options
+ * Specified options
+ * @see Base64#GZIP
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public static String encodeBytes(byte[] source, int off, int len,
+ int options) {
+ // Isolate options
+ int dontBreakLines = (options & DONT_BREAK_LINES);
+ int gzip = (options & GZIP);
+
+ // Compress?
+ if (gzip == GZIP) {
+ java.io.ByteArrayOutputStream baos = null;
+ java.util.zip.GZIPOutputStream gzos = null;
+ Base64.OutputStream b64os = null;
+
+ try {
+ // GZip -> Base64 -> ByteArray
+ baos = new java.io.ByteArrayOutputStream();
+ b64os = new Base64.OutputStream(baos, ENCODE | dontBreakLines);
+ gzos = new java.util.zip.GZIPOutputStream(b64os);
+
+ gzos.write(source, off, len);
+ gzos.close();
+ } // end try
+ catch (java.io.IOException e) {
+ e.printStackTrace();
+ return null;
+ } // end catch
+ finally {
+ try {
+ gzos.close();
+ } catch (Exception e) {
+ }
+ try {
+ b64os.close();
+ } catch (Exception e) {
+ }
+ try {
+ baos.close();
+ } catch (Exception e) {
+ }
+ } // end finally
+
+ // Return value according to relevant encoding.
+ try {
+ return new String(baos.toByteArray(), PREFERRED_ENCODING);
+ } // end try
+ catch (java.io.UnsupportedEncodingException uue) {
+ return new String(baos.toByteArray());
+ } // end catch
+ } // end if: compress
+
+ // Else, don't compress. Better not to use streams at all then.
+ else {
+ // Convert option to boolean in way that code likes it.
+ boolean breakLines = dontBreakLines == 0;
+
+ int len43 = len * 4 / 3;
+ byte[] outBuff = new byte[(len43) // Main 4:3
+ + ((len % 3) > 0 ? 4 : 0) // Account for padding
+ + (breakLines ? (len43 / MAX_LINE_LENGTH) : 0)]; // New
+ // lines
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for (; d < len2; d += 3, e += 4) {
+ encode3to4(source, d + off, 3, outBuff, e);
+
+ lineLength += 4;
+ if (breakLines && lineLength == MAX_LINE_LENGTH) {
+ outBuff[e + 4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // en dfor: each piece of array
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e);
+ e += 4;
+ } // end if: some padding needed
+
+ // Return value according to relevant encoding.
+ try {
+ return new String(outBuff, 0, e, PREFERRED_ENCODING);
+ } // end try
+ catch (java.io.UnsupportedEncodingException uue) {
+ return new String(outBuff, 0, e);
+ } // end catch
+
+ } // end else: don't compress
+
+ } // end encodeBytes
+
+ /* ******** D E C O D I N G M E T H O D S ******** */
+
+ /**
+ * Decodes four bytes from array <var>source</var> and writes the resulting
+ * bytes (up to three of them) to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
+ * does not check to make sure your arrays are large enough to accomodate
+ * <var>srcOffset</var> + 4 for the <var>source</var> array or
+ * <var>destOffset</var> + 3 for the <var>destination</var> array. This
+ * method returns the actual number of bytes that were converted from the
+ * Base64 encoding.
+ *
+ * @param source
+ * the array to convert
+ * @param srcOffset
+ * the index where conversion begins
+ * @param destination
+ * the array to hold the conversion
+ * @param destOffset
+ * the index where output will be put
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3(byte[] source, int srcOffset,
+ byte[] destination, int destOffset) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ // Two ways to do the same thing. Don't know which way I like best.
+ // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6
+ // )
+ // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
+ int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
+ | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ }
+
+ // Example: DkL=
+ else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ // Two ways to do the same thing. Don't know which way I like best.
+ // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6
+ // )
+ // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
+ // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
+ int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
+ | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
+ | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6);
+
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ }
+
+ // Example: DkLE
+ else {
+ try {
+ // Two ways to do the same thing. Don't know which way I like
+ // best.
+ // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 )
+ // >>> 6 )
+ // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
+ // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
+ // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
+ int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18)
+ | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12)
+ | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6)
+ | ((DECODABET[source[srcOffset + 3]] & 0xFF));
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+
+ return 3;
+ } catch (Exception e) {
+ System.out.println("" + source[srcOffset] + ": "
+ + (DECODABET[source[srcOffset]]));
+ System.out.println("" + source[srcOffset + 1] + ": "
+ + (DECODABET[source[srcOffset + 1]]));
+ System.out.println("" + source[srcOffset + 2] + ": "
+ + (DECODABET[source[srcOffset + 2]]));
+ System.out.println("" + source[srcOffset + 3] + ": "
+ + (DECODABET[source[srcOffset + 3]]));
+ return -1;
+ } // e nd catch
+ }
+ } // end decodeToBytes
+
+ /**
+ * Very low-level access to decoding ASCII characters in the form of a byte
+ * array. Does not support automatically gunzipping or any other "fancy"
+ * features.
+ *
+ * @param source
+ * The Base64 encoded data
+ * @param off
+ * The offset of where to begin decoding
+ * @param len
+ * The length of characters to decode
+ * @return decoded data
+ * @since 1.3
+ */
+ public static byte[] decode(byte[] source, int off, int len) {
+ int len34 = len * 3 / 4;
+ byte[] outBuff = new byte[len34]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+ int i = 0;
+ byte sbiCrop = 0;
+ byte sbiDecode = 0;
+ for (i = off; i < off + len; i++) {
+ sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits
+ sbiDecode = DECODABET[sbiCrop];
+
+ if (sbiDecode >= WHITE_SPACE_ENC) // White space, Equals sign or
+ // better
+ {
+ if (sbiDecode >= EQUALS_SIGN_ENC) {
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn > 3) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn);
+ b4Posn = 0;
+
+ // If that was the equals sign, break out of 'for' loop
+ if (sbiCrop == EQUALS_SIGN) {
+ break;
+ }
+ } // end if: quartet built
+
+ } // end if: equals sign or better
+
+ } // end if: white space, equals sign or better
+ else {
+ System.err.println("Bad Base64 input character at " + i + ": "
+ + source[i] + "(decimal)");
+ return null;
+ } // end else:
+ } // each input character
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ } // end decode
+
+ /**
+ * Decodes data from Base64 notation, automatically detecting
+ * gzip-compressed data and decompressing it.
+ *
+ * @param s
+ * the string to decode
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode(String s) {
+ byte[] bytes;
+ try {
+ bytes = s.getBytes(PREFERRED_ENCODING);
+ } // end try
+ catch (java.io.UnsupportedEncodingException uee) {
+ bytes = s.getBytes();
+ } // end catch
+ // </change>
+
+ // Decode
+ bytes = decode(bytes, 0, bytes.length);
+
+ // Check to see if it's gzip-compressed
+ // GZIP Magic Two-Byte Number: 0x8b1f (35615)
+ if (bytes != null && bytes.length >= 4) {
+
+ int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
+ if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) {
+ java.io.ByteArrayInputStream bais = null;
+ java.util.zip.GZIPInputStream gzis = null;
+ java.io.ByteArrayOutputStream baos = null;
+ byte[] buffer = new byte[2048];
+ int length = 0;
+
+ try {
+ baos = new java.io.ByteArrayOutputStream();
+ bais = new java.io.ByteArrayInputStream(bytes);
+ gzis = new java.util.zip.GZIPInputStream(bais);
+
+ while ((length = gzis.read(buffer)) >= 0) {
+ baos.write(buffer, 0, length);
+ } // end while: reading input
+
+ // No error? Get new bytes.
+ bytes = baos.toByteArray();
+
+ } // end try
+ catch (java.io.IOException e) {
+ // Just return originally-decoded bytes
+ } // end catch
+ finally {
+ try {
+ baos.close();
+ } catch (Exception e) {
+ }
+ try {
+ gzis.close();
+ } catch (Exception e) {
+ }
+ try {
+ bais.close();
+ } catch (Exception e) {
+ }
+ } // end finally
+
+ } // end if: gzipped
+ } // end if: bytes.length >= 2
+
+ return bytes;
+ } // end decode
+
+ /**
+ * Attempts to decode Base64 data and deserialize a Java Object within.
+ * Returns <tt>null</tt> if there was an error.
+ *
+ * @param encodedObject
+ * The Base64 data to decode
+ * @return The decoded and deserialized object
+ * @since 1.5
+ */
+ public static Object decodeToObject(String encodedObject) {
+ // Decode and gunzip if necessary
+ byte[] objBytes = decode(encodedObject);
+
+ java.io.ByteArrayInputStream bais = null;
+ java.io.ObjectInputStream ois = null;
+ Object obj = null;
+
+ try {
+ bais = new java.io.ByteArrayInputStream(objBytes);
+ ois = new java.io.ObjectInputStream(bais);
+
+ obj = ois.readObject();
+ } // end try
+ catch (java.io.IOException e) {
+ e.printStackTrace();
+ obj = null;
+ } // end catch
+ catch (java.lang.ClassNotFoundException e) {
+ e.printStackTrace();
+ obj = null;
+ } // end catch
+ finally {
+ try {
+ bais.close();
+ } catch (Exception e) {
+ }
+ try {
+ ois.close();
+ } catch (Exception e) {
+ }
+ } // end finally
+
+ return obj;
+ } // end decodeObject
+
+ /**
+ * Convenience method for encoding data to a file.
+ *
+ * @param dataToEncode
+ * byte array of data to encode in base64 form
+ * @param filename
+ * Filename for saving encoded data
+ * @return <tt>true</tt> if successful, <tt>false</tt> otherwise
+ * @since 2.1
+ */
+ public static boolean encodeToFile(byte[] dataToEncode, String filename) {
+ boolean success = false;
+ Base64.OutputStream bos = null;
+ try {
+ bos = new Base64.OutputStream(
+ new java.io.FileOutputStream(filename), Base64.ENCODE);
+ bos.write(dataToEncode);
+ success = true;
+ } // end try
+ catch (java.io.IOException e) {
+
+ success = false;
+ } // end catch: IOException
+ finally {
+ try {
+ bos.close();
+ } catch (Exception e) {
+ }
+ } // end finally
+
+ return success;
+ } // end encodeToFile
+
+ /**
+ * Convenience method for decoding data to a file.
+ *
+ * @param dataToDecode
+ * Base64-encoded data as a string
+ * @param filename
+ * Filename for saving decoded data
+ * @return <tt>true</tt> if successful, <tt>false</tt> otherwise
+ * @since 2.1
+ */
+ public static boolean decodeToFile(String dataToDecode, String filename) {
+ boolean success = false;
+ Base64.OutputStream bos = null;
+ try {
+ bos = new Base64.OutputStream(
+ new java.io.FileOutputStream(filename), Base64.DECODE);
+ bos.write(dataToDecode.getBytes(PREFERRED_ENCODING));
+ success = true;
+ } // end try
+ catch (java.io.IOException e) {
+ success = false;
+ } // end catch: IOException
+ finally {
+ try {
+ bos.close();
+ } catch (Exception e) {
+ }
+ } // end finally
+
+ return success;
+ } // end decodeToFile
+
+ /**
+ * Convenience method for reading a base64-encoded file and decoding it.
+ *
+ * @param filename
+ * Filename for reading encoded data
+ * @return decoded byte array or null if unsuccessful
+ * @since 2.1
+ */
+ public static byte[] decodeFromFile(String filename) {
+ byte[] decodedData = null;
+ Base64.InputStream bis = null;
+ try {
+ // Set up some useful variables
+ java.io.File file = new java.io.File(filename);
+ byte[] buffer = null;
+ int length = 0;
+ int numBytes = 0;
+
+ // Check for size of file
+ if (file.length() > Integer.MAX_VALUE) {
+ System.err
+ .println("File is too big for this convenience method ("
+ + file.length() + " bytes).");
+ return null;
+ } // end if: file too big for int index
+ buffer = new byte[(int) file.length()];
+
+ // Open a stream
+ bis = new Base64.InputStream(new java.io.BufferedInputStream(
+ new java.io.FileInputStream(file)), Base64.DECODE);
+
+ // Read until done
+ while ((numBytes = bis.read(buffer, length, 4096)) >= 0) {
+ length += numBytes;
+ }
+
+ // Save in a variable to return
+ decodedData = new byte[length];
+ System.arraycopy(buffer, 0, decodedData, 0, length);
+
+ } // end try
+ catch (java.io.IOException e) {
+ System.err.println("Error decoding from file " + filename);
+ } // end catch: IOException
+ finally {
+ try {
+ bis.close();
+ } catch (Exception e) {
+ }
+ } // end finally
+
+ return decodedData;
+ } // end decodeFromFile
+
+ /**
+ * Convenience method for reading a binary file and base64-encoding it.
+ *
+ * @param filename
+ * Filename for reading binary data
+ * @return base64-encoded string or null if unsuccessful
+ * @since 2.1
+ */
+ public static String encodeFromFile(String filename) {
+ String encodedData = null;
+ Base64.InputStream bis = null;
+ try {
+ // Set up some useful variables
+ java.io.File file = new java.io.File(filename);
+ byte[] buffer = new byte[(int) (file.length() * 1.4)];
+ int length = 0;
+ int numBytes = 0;
+
+ // Open a stream
+ bis = new Base64.InputStream(new java.io.BufferedInputStream(
+ new java.io.FileInputStream(file)), Base64.ENCODE);
+
+ // Read until done
+ while ((numBytes = bis.read(buffer, length, 4096)) >= 0) {
+ length += numBytes;
+ }
+
+ // Save in a variable to return
+ encodedData = new String(buffer, 0, length,
+ Base64.PREFERRED_ENCODING);
+
+ } // end try
+ catch (java.io.IOException e) {
+ System.err.println("Error encoding from file " + filename);
+ } // end catch: IOException
+ finally {
+ try {
+ bis.close();
+ } catch (Exception e) {
+ }
+ } // end finally
+
+ return encodedData;
+ } // end encodeFromFile
+
+ /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */
+
+ /**
+ * A {@link Base64.InputStream} will read data from another
+ * <tt>java.io.InputStream</tt>, given in the constructor, and
+ * encode/decode to/from Base64 notation on the fly.
+ *
+ * @see Base64
+ * @since 1.3
+ */
+ public static class InputStream extends java.io.FilterInputStream {
+ private boolean encode; // Encoding or decoding
+
+ private int position; // Current position in the buffer
+
+ private byte[] buffer; // Small buffer holding converted data
+
+ private int bufferLength; // Length of buffer (3 or 4)
+
+ private int numSigBytes; // Number of meaningful bytes in the buffer
+
+ private int lineLength;
+
+ private boolean breakLines; // Break lines at less than 80 characters
+
+ /**
+ * Constructs a {@link Base64.InputStream} in DECODE mode.
+ *
+ * @param in
+ * the <tt>java.io.InputStream</tt> from which to read
+ * data.
+ * @since 1.3
+ */
+ public InputStream(java.io.InputStream in) {
+ this(in, DECODE);
+ } // end constructor
+
+ /**
+ * Constructs a {@link Base64.InputStream} in either ENCODE or DECODE
+ * mode. <p/> Valid options:
+ *
+ * <pre>
+ * ENCODE or DECODE: Encode or Decode as data is read.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * (only meaningful when encoding)
+ * &lt;i&gt;Note: Technically, this makes your encoding non-compliant.&lt;/i&gt;
+ * </pre>
+ *
+ * <p/> Example:
+ * <code>new Base64.InputStream( in, Base64.DECODE )</code>
+ *
+ * @param in
+ * the <tt>java.io.InputStream</tt> from which to read
+ * data.
+ * @param options
+ * Specified options
+ * @see Base64#ENCODE
+ * @see Base64#DECODE
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public InputStream(java.io.InputStream in, int options) {
+ super(in);
+ this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
+ this.encode = (options & ENCODE) == ENCODE;
+ this.bufferLength = encode ? 4 : 3;
+ this.buffer = new byte[bufferLength];
+ this.position = -1;
+ this.lineLength = 0;
+ } // end constructor
+
+ /**
+ * Reads enough of the input stream to convert to/from Base64 and
+ * returns the next byte.
+ *
+ * @return next byte
+ * @since 1.3
+ */
+ public int read() throws java.io.IOException {
+ // Do we need to get data?
+ if (position < 0) {
+ if (encode) {
+ byte[] b3 = new byte[3];
+ int numBinaryBytes = 0;
+ for (int i = 0; i < 3; i++) {
+ try {
+ int b = in.read();
+
+ // If end of stream, b is -1.
+ if (b >= 0) {
+ b3[i] = (byte) b;
+ numBinaryBytes++;
+ } // end if: not end of stream
+
+ } // end try: read
+ catch (java.io.IOException e) {
+ // Only a problem if we got no data at all.
+ if (i == 0) {
+ throw e;
+ }
+
+ } // end catch
+ } // end for: each needed input byte
+
+ if (numBinaryBytes > 0) {
+ encode3to4(b3, 0, numBinaryBytes, buffer, 0);
+ position = 0;
+ numSigBytes = 4;
+ } // end if: got data
+ else {
+ return -1;
+ } // end else
+ } // end if: encoding
+
+ // Else decoding
+ else {
+ byte[] b4 = new byte[4];
+ int i = 0;
+ for (i = 0; i < 4; i++) {
+ // Read four "meaningful" bytes:
+ int b = 0;
+ do {
+ b = in.read();
+ } while (b >= 0
+ && DECODABET[b & 0x7f] <= WHITE_SPACE_ENC);
+
+ if (b < 0) {
+ break; // Reads a -1 if end of stream
+ }
+
+ b4[i] = (byte) b;
+ } // end for: each needed input byte
+
+ if (i == 4) {
+ numSigBytes = decode4to3(b4, 0, buffer, 0);
+ position = 0;
+ } // end if: got four characters
+ else if (i == 0) {
+ return -1;
+ } // end else if: also padded correctly
+ else {
+ // Must have broken out from above.
+ throw new java.io.IOException(
+ "Improperly padded Base64 input.");
+ } // end
+
+ } // end else: decode
+ } // end else: get data
+
+ // Got data?
+ if (position >= 0) {
+ // End of relevant data?
+ if (/* !encode && */position >= numSigBytes) {
+ return -1;
+ }
+
+ if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) {
+ lineLength = 0;
+ return '\n';
+ } // end if
+ else {
+ lineLength++; // This isn't important when decoding
+ // but throwing an extra "if" seems
+ // just as wasteful.
+
+ int b = buffer[position++];
+
+ if (position >= bufferLength) {
+ position = -1;
+ }
+
+ return b & 0xFF; // This is how you "cast" a byte that's
+ // intended to be unsigned.
+ } // end else
+ } // end if: position >= 0
+
+ // Else error
+ else {
+ // When JDK1.4 is more accepted, use an assertion here.
+ throw new java.io.IOException(
+ "Error in Base64 code reading stream.");
+ } // end else
+ } // end read
+
+ /**
+ * Calls {@link #read()} repeatedly until the end of stream is reached
+ * or <var>len</var> bytes are read. Returns number of bytes read into
+ * array or -1 if end of stream is encountered.
+ *
+ * @param dest
+ * array to hold values
+ * @param off
+ * offset for array
+ * @param len
+ * max number of bytes to read into array
+ * @return bytes read into array or -1 if end of stream is encountered.
+ * @since 1.3
+ */
+ public int read(byte[] dest, int off, int len)
+ throws java.io.IOException {
+ int i;
+ int b;
+ for (i = 0; i < len; i++) {
+ b = read();
+
+ // if( b < 0 && i == 0 )
+ // return -1;
+
+ if (b >= 0) {
+ dest[off + i] = (byte) b;
+ } else if (i == 0) {
+ return -1;
+ } else {
+ break; // Out of 'for' loop
+ }
+ } // end for: each byte read
+ return i;
+ } // end read
+
+ } // end inner class InputStream
+
+ /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */
+
+ /**
+ * A {@link Base64.OutputStream} will write data to another
+ * <tt>java.io.OutputStream</tt>, given in the constructor, and
+ * encode/decode to/from Base64 notation on the fly.
+ *
+ * @see Base64
+ * @since 1.3
+ */
+ public static class OutputStream extends java.io.FilterOutputStream {
+ private boolean encode;
+
+ private int position;
+
+ private byte[] buffer;
+
+ private int bufferLength;
+
+ private int lineLength;
+
+ private boolean breakLines;
+
+ private byte[] b4; // Scratch used in a few places
+
+ private boolean suspendEncoding;
+
+ /**
+ * Constructs a {@link Base64.OutputStream} in ENCODE mode.
+ *
+ * @param out
+ * the <tt>java.io.OutputStream</tt> to which data will be
+ * written.
+ * @since 1.3
+ */
+ public OutputStream(java.io.OutputStream out) {
+ this(out, ENCODE);
+ } // end constructor
+
+ /**
+ * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE
+ * mode. <p/> Valid options:
+ *
+ * <pre>
+ * ENCODE or DECODE: Encode or Decode as data is read.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * (only meaningful when encoding)
+ * &lt;i&gt;Note: Technically, this makes your encoding non-compliant.&lt;/i&gt;
+ * </pre>
+ *
+ * <p/> Example:
+ * <code>new Base64.OutputStream( out, Base64.ENCODE )</code>
+ *
+ * @param out
+ * the <tt>java.io.OutputStream</tt> to which data will be
+ * written.
+ * @param options
+ * Specified options.
+ * @see Base64#ENCODE
+ * @see Base64#DECODE
+ * @see Base64#DONT_BREAK_LINES
+ * @since 1.3
+ */
+ public OutputStream(java.io.OutputStream out, int options) {
+ super(out);
+ this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
+ this.encode = (options & ENCODE) == ENCODE;
+ this.bufferLength = encode ? 3 : 4;
+ this.buffer = new byte[bufferLength];
+ this.position = 0;
+ this.lineLength = 0;
+ this.suspendEncoding = false;
+ this.b4 = new byte[4];
+ } // end constructor
+
+ /**
+ * Writes the byte to the output stream after converting to/from Base64
+ * notation. When encoding, bytes are buffered three at a time before
+ * the output stream actually gets a write() call. When decoding, bytes
+ * are buffered four at a time.
+ *
+ * @param theByte
+ * the byte to write
+ * @since 1.3
+ */
+ public void write(int theByte) throws java.io.IOException {
+ // Encoding suspended?
+ if (suspendEncoding) {
+ super.out.write(theByte);
+ return;
+ } // end if: supsended
+
+ // Encode?
+ if (encode) {
+ buffer[position++] = (byte) theByte;
+ if (position >= bufferLength) // Enough to encode.
+ {
+ out.write(encode3to4(b4, buffer, bufferLength));
+
+ lineLength += 4;
+ if (breakLines && lineLength >= MAX_LINE_LENGTH) {
+ out.write(NEW_LINE);
+ lineLength = 0;
+ } // end if: end of line
+
+ position = 0;
+ } // end if: enough to output
+ } // end if: encoding
+
+ // Else, Decoding
+ else {
+ // Meaningful Base64 character?
+ if (DECODABET[theByte & 0x7f] > WHITE_SPACE_ENC) {
+ buffer[position++] = (byte) theByte;
+ if (position >= bufferLength) // Enough to output.
+ {
+ int len = Base64.decode4to3(buffer, 0, b4, 0);
+ out.write(b4, 0, len);
+ // out.write( Base64.decode4to3( buffer ) );
+ position = 0;
+ } // end if: enough to output
+ } // end if: meaningful base64 character
+ else if (DECODABET[theByte & 0x7f] != WHITE_SPACE_ENC) {
+ throw new java.io.IOException(
+ "Invalid character in Base64 data.");
+ } // end else: not white space either
+ } // end else: decoding
+ } // end write
+
+ /**
+ * Calls {@link #write(int)} repeatedly until <var>len</var> bytes are
+ * written.
+ *
+ * @param theBytes
+ * array from which to read bytes
+ * @param off
+ * offset for array
+ * @param len
+ * max number of bytes to read into array
+ * @since 1.3
+ */
+ public void write(byte[] theBytes, int off, int len)
+ throws java.io.IOException {
+ // Encoding suspended?
+ if (suspendEncoding) {
+ super.out.write(theBytes, off, len);
+ return;
+ } // end if: supsended
+
+ for (int i = 0; i < len; i++) {
+ write(theBytes[off + i]);
+ } // end for: each byte written
+
+ } // end write
+
+ /**
+ * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer
+ * without closing the stream.
+ */
+ public void flushBase64() throws java.io.IOException {
+ if (position > 0) {
+ if (encode) {
+ out.write(encode3to4(b4, buffer, position));
+ position = 0;
+ } // end if: encoding
+ else {
+ throw new java.io.IOException(
+ "Base64 input not properly padded.");
+ } // end else: decoding
+ } // end if: buffer partially full
+
+ } // end flush
+
+ /**
+ * Flushes and closes (I think, in the superclass) the stream.
+ *
+ * @since 1.3
+ */
+ public void close() throws java.io.IOException {
+ // 1. Ensure that pending characters are written
+ flushBase64();
+
+ // 2. Actually close the stream
+ // Base class both flushes and closes.
+ super.close();
+
+ buffer = null;
+ out = null;
+ } // end close
+
+ /**
+ * Suspends encoding of the stream. May be helpful if you need to embed
+ * a piece of base640-encoded data in a stream.
+ *
+ * @since 1.5.1
+ */
+ public void suspendEncoding() throws java.io.IOException {
+ flushBase64();
+ this.suspendEncoding = true;
+ } // end suspendEncoding
+
+ /**
+ * Resumes encoding of the stream. May be helpful if you need to embed a
+ * piece of base640-encoded data in a stream.
+ *
+ * @since 1.5.1
+ */
+ public void resumeEncoding() {
+ this.suspendEncoding = false;
+ } // end resumeEncoding
+
+ } // end inner class OutputStream
+
+} // end class Base64
+
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java
new file mode 100644
index 000000000..11a84d471
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java
@@ -0,0 +1,130 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.OrFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * The fault tolerant negotiator takes two stream negotiators, the primary and the secondary negotiator.
+ * If the primary negotiator fails during the stream negotiaton process, the second negotiator is used.
+ */
+public class FaultTolerantNegotiator extends StreamNegotiator {
+
+ private StreamNegotiator primaryNegotiator;
+ private StreamNegotiator secondaryNegotiator;
+ private XMPPConnection connection;
+ private PacketFilter primaryFilter;
+ private PacketFilter secondaryFilter;
+
+ public FaultTolerantNegotiator(XMPPConnection connection, StreamNegotiator primary, StreamNegotiator secondary) {
+ this.primaryNegotiator = primary;
+ this.secondaryNegotiator = secondary;
+ this.connection = connection;
+ }
+
+ public PacketFilter getInitiationPacketFilter(String from, String streamID) {
+ if (primaryFilter == null || secondaryFilter == null) {
+ primaryFilter = primaryNegotiator.getInitiationPacketFilter(from, streamID);
+ secondaryFilter = secondaryNegotiator.getInitiationPacketFilter(from, streamID);
+ }
+ return new OrFilter(primaryFilter, secondaryFilter);
+ }
+
+ InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException {
+ throw new UnsupportedOperationException("Negotiation only handled by create incoming stream method.");
+ }
+
+ final Packet initiateIncomingStream(XMPPConnection connection, StreamInitiation initiation) throws XMPPException {
+ throw new UnsupportedOperationException("Initiation handled by createIncomingStream method");
+ }
+
+ public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException {
+ PacketFilter filter = getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID());
+ PacketCollector collector = connection.createPacketCollector(filter);
+
+ StreamInitiation response = super.createInitiationAccept(initiation, getNamespaces());
+ connection.sendPacket(response);
+
+ InputStream stream;
+ try {
+ Packet streamInitiation = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ if (streamInitiation == null) {
+ throw new XMPPException("No response from remote client");
+ }
+ StreamNegotiator negotiator = determineNegotiator(streamInitiation);
+ stream = negotiator.negotiateIncomingStream(streamInitiation);
+ }
+ catch (XMPPException ex) {
+ ex.printStackTrace();
+ Packet streamInitiation = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (streamInitiation == null) {
+ throw new XMPPException("No response from remote client");
+ }
+ StreamNegotiator negotiator = determineNegotiator(streamInitiation);
+ stream = negotiator.negotiateIncomingStream(streamInitiation);
+ } finally {
+ collector.cancel();
+ }
+
+ return stream;
+ }
+
+ private StreamNegotiator determineNegotiator(Packet streamInitiation) {
+ return primaryFilter.accept(streamInitiation) ? primaryNegotiator : secondaryNegotiator;
+ }
+
+ public OutputStream createOutgoingStream(String streamID, String initiator, String target) throws XMPPException {
+ OutputStream stream;
+ try {
+ stream = primaryNegotiator.createOutgoingStream(streamID, initiator, target);
+ }
+ catch (XMPPException ex) {
+ stream = secondaryNegotiator.createOutgoingStream(streamID, initiator, target);
+ }
+
+ return stream;
+ }
+
+ public String[] getNamespaces() {
+ String [] primary = primaryNegotiator.getNamespaces();
+ String [] secondary = secondaryNegotiator.getNamespaces();
+
+ String [] namespaces = new String[primary.length + secondary.length];
+ System.arraycopy(primary, 0, namespaces, 0, primary.length);
+ System.arraycopy(secondary, 0, namespaces, primary.length, secondary.length);
+
+ return namespaces;
+ }
+
+ public void cleanup() {
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java
new file mode 100644
index 000000000..c58d44736
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java
@@ -0,0 +1,353 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.jivesoftware.smack.XMPPException;
+
+/**
+ * Contains the generic file information and progress related to a particular
+ * file transfer.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public abstract class FileTransfer {
+
+ private String fileName;
+
+ private String filePath;
+
+ private long fileSize;
+
+ private String peer;
+
+ private org.jivesoftware.smackx.filetransfer.FileTransfer.Status status;
+
+ protected FileTransferNegotiator negotiator;
+
+ protected String streamID;
+
+ protected long amountWritten = -1;
+
+ private Error error;
+
+ private Exception exception;
+
+ protected FileTransfer(String peer, String streamID,
+ FileTransferNegotiator negotiator) {
+ this.peer = peer;
+ this.streamID = streamID;
+ this.negotiator = negotiator;
+ }
+
+ protected void setFileInfo(String fileName, long fileSize) {
+ this.fileName = fileName;
+ this.fileSize = fileSize;
+ }
+
+ protected void setFileInfo(String path, String fileName, long fileSize) {
+ this.filePath = path;
+ this.fileName = fileName;
+ this.fileSize = fileSize;
+ }
+
+ /**
+ * Returns the size of the file being transfered.
+ *
+ * @return Returns the size of the file being transfered.
+ */
+ public long getFileSize() {
+ return fileSize;
+ }
+
+ /**
+ * Returns the name of the file being transfered.
+ *
+ * @return Returns the name of the file being transfered.
+ */
+ public String getFileName() {
+ return fileName;
+ }
+
+ /**
+ * Returns the local path of the file.
+ *
+ * @return Returns the local path of the file.
+ */
+ public String getFilePath() {
+ return filePath;
+ }
+
+ /**
+ * Returns the JID of the peer for this file transfer.
+ *
+ * @return Returns the JID of the peer for this file transfer.
+ */
+ public String getPeer() {
+ return peer;
+ }
+
+ /**
+ * Returns the progress of the file transfer as a number between 0 and 1.
+ *
+ * @return Returns the progress of the file transfer as a number between 0
+ * and 1.
+ */
+ public double getProgress() {
+ if(amountWritten == 0) {
+ return 0;
+ }
+ return amountWritten / fileSize;
+ }
+
+ /**
+ * Returns true if the transfer has been cancled, if it has stopped because
+ * of a an error, or the transfer completed succesfully.
+ *
+ * @return Returns true if the transfer has been cancled, if it has stopped
+ * because of a an error, or the transfer completed succesfully.
+ */
+ public boolean isDone() {
+ return status == Status.CANCLED || status == Status.ERROR
+ || status == Status.COMPLETE;
+ }
+
+ /**
+ * Retuns the current status of the file transfer.
+ *
+ * @return Retuns the current status of the file transfer.
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ protected void setError(Error type) {
+ this.error = type;
+ }
+
+ /**
+ * When {@link #getStatus()} returns that there was an {@link Status#ERROR}
+ * during the transfer, the type of error can be retrieved through this
+ * method.
+ *
+ * @return Returns the type of error that occured if one has occured.
+ */
+ public Error getError() {
+ return error;
+ }
+
+ /**
+ * If an exception occurs asynchronously it will be stored for later
+ * retrival. If there is an error there maybe an exception set.
+ *
+ * @return The exception that occured or null if there was no exception.
+ * @see #getError()
+ */
+ public Exception getException() {
+ return exception;
+ }
+
+ /**
+ * Cancels the file transfer.
+ */
+ public abstract void cancel();
+
+ protected void setException(Exception exception) {
+ this.exception = exception;
+ }
+
+ protected final void setStatus(Status status) {
+ this.status = status;
+ }
+
+ protected void writeToStream(final InputStream in, final OutputStream out)
+ throws XMPPException {
+ final byte[] b = new byte[1000];
+ int count = 0;
+ amountWritten = 0;
+ try {
+ count = in.read(b);
+ } catch (IOException e) {
+ throw new XMPPException("error reading from input stream", e);
+ }
+ while (count != -1 && !getStatus().equals(Status.CANCLED)) {
+ if (getStatus().equals(Status.CANCLED)) {
+ return;
+ }
+
+ // write to the output stream
+ try {
+ out.write(b, 0, count);
+ } catch (IOException e) {
+ throw new XMPPException("error writing to output stream", e);
+ }
+
+ amountWritten += count;
+
+ // read more bytes from the input stream
+ try {
+ count = in.read(b);
+ } catch (IOException e) {
+ throw new XMPPException("error reading from input stream", e);
+ }
+ }
+
+ // the connection was likely terminated abrubtly if these are not
+ // equal
+ if (!getStatus().equals(Status.CANCLED) && getError() == Error.NONE
+ && amountWritten != fileSize) {
+ this.error = Error.CONNECTION;
+ }
+ }
+
+ /**
+ * A class to represent the current status of the file transfer.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public static class Status {
+ /**
+ * An error occured during the transfer.
+ *
+ * @see FileTransfer#getError()
+ */
+ public static final Status ERROR = new Status();
+
+ /**
+ * The file transfer is being negotiated with the peer. The party
+ * recieving the file has the option to accept or refuse a file transfer
+ * request. If they accept, then the process of stream negotiation will
+ * begin. If they refuse the file will not be transfered.
+ *
+ * @see #NEGOTIATING_STREAM
+ */
+ public static final Status NEGOTIATING_TRANSFER = new Status();
+
+ /**
+ * The peer has refused the file transfer request halting the file
+ * transfer negotiation process.
+ */
+ public static final Status REFUSED = new Status();
+
+ /**
+ * The stream to transfer the file is being negotiated over the chosen
+ * stream type. After the stream negotiating process is complete the
+ * status becomes negotiated.
+ *
+ * @see #NEGOTIATED
+ */
+ public static final Status NEGOTIATING_STREAM = new Status();
+
+ /**
+ * After the stream negotitation has completed the intermediate state
+ * between the time when the negotiation is finished and the actual
+ * transfer begins.
+ */
+ public static final Status NEGOTIATED = new Status();
+
+ /**
+ * The transfer is in progress.
+ *
+ * @see FileTransfer#getProgress()
+ */
+ public static final Status IN_PROGRESS = new Status();
+
+ /**
+ * The transfer has completed successfully.
+ */
+ public static final Status COMPLETE = new Status();
+
+ /**
+ * The file transfer was canceled
+ */
+ public static final Status CANCLED = new Status();
+ }
+
+ /**
+ * Return the length of bytes written out to the stream.
+ * @return the amount in bytes written out.
+ */
+ public long getAmountWritten(){
+ return amountWritten;
+ }
+
+ public static class Error {
+ /**
+ * No error
+ */
+ public static final Error NONE = new Error("No error");
+
+ /**
+ * The peer did not find any of the provided stream mechanisms
+ * acceptable.
+ */
+ public static final Error NOT_ACCEPTABLE = new Error(
+ "The peer did not find any of the provided stream mechanisms acceptable.");
+
+ /**
+ * The provided file to transfer does not exist or could not be read.
+ */
+ public static final Error BAD_FILE = new Error(
+ "The provided file to transfer does not exist or could not be read.");
+
+ /**
+ * The remote user did not respond or the connection timed out.
+ */
+ public static final Error NO_RESPONSE = new Error(
+ "The remote user did not respond or the connection timed out.");
+
+ /**
+ * An error occured over the socket connected to send the file.
+ */
+ public static final Error CONNECTION = new Error(
+ "An error occured over the socket connected to send the file.");
+
+ /**
+ * An error occured while sending or recieving the file
+ */
+ protected static final Error STREAM = new Error(
+ "An error occured while sending or recieving the file");
+
+ private final String msg;
+
+ private Error(String msg) {
+ this.msg = msg;
+ }
+
+ /**
+ * Returns a String representation of this error.
+ *
+ * @return Returns a String representation of this error.
+ */
+ public String getMessage() {
+ return msg;
+ }
+
+ public String toString() {
+ return msg;
+ }
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java
new file mode 100644
index 000000000..904623cb9
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java
@@ -0,0 +1,36 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+/**
+ * File transfers can cause several events to be raised. These events can be
+ * monitored through this interface.
+ *
+ * @author Alexander Wenckus
+ */
+public interface FileTransferListener {
+ /**
+ * A request to send a file has been recieved from another user.
+ *
+ * @param request
+ * The request from the other user.
+ */
+ public void fileTransferRequest(final FileTransferRequest request);
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java
new file mode 100644
index 000000000..de4f7f10a
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java
@@ -0,0 +1,178 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.IQTypeFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+/**
+ * The file transfer manager class handles the sending and recieving of files.
+ * To send a file invoke the {@link #createOutgoingFileTransfer(String)} method.
+ * <p>
+ * And to recieve a file add a file transfer listener to the manager. The
+ * listener will notify you when there is a new file transfer request. To create
+ * the {@link IncomingFileTransfer} object accept the transfer, or, if the
+ * transfer is not desirable reject it.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class FileTransferManager {
+
+ private final FileTransferNegotiator fileTransferNegotiator;
+
+ private List listeners;
+
+ private XMPPConnection connection;
+
+ /**
+ * Creates a file transfer manager to initiate and receive file transfers.
+ *
+ * @param connection
+ * The XMPPConnection that the file transfers will use.
+ */
+ public FileTransferManager(XMPPConnection connection) {
+ this.connection = connection;
+ this.fileTransferNegotiator = FileTransferNegotiator
+ .getInstanceFor(connection);
+ }
+
+ /**
+ * Add a file transfer listener to listen to incoming file transfer
+ * requests.
+ *
+ * @param li
+ * The listener
+ * @see #removeFileTransferListener(FileTransferListener)
+ * @see FileTransferListener
+ */
+ public void addFileTransferListener(final FileTransferListener li) {
+ if (listeners == null) {
+ initListeners();
+ }
+ synchronized (this.listeners) {
+ listeners.add(li);
+ }
+ }
+
+ private void initListeners() {
+ listeners = new ArrayList();
+
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ fireNewRequest((StreamInitiation) packet);
+ }
+ }, new AndFilter(new PacketTypeFilter(StreamInitiation.class),
+ new IQTypeFilter(IQ.Type.SET)));
+ }
+
+ protected void fireNewRequest(StreamInitiation initiation) {
+ FileTransferListener[] listeners = null;
+ synchronized (this.listeners) {
+ listeners = new FileTransferListener[this.listeners.size()];
+ this.listeners.toArray(listeners);
+ }
+ FileTransferRequest request = new FileTransferRequest(this, initiation);
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].fileTransferRequest(request);
+ }
+ }
+
+ /**
+ * Removes a file transfer listener.
+ *
+ * @param li
+ * The file transfer listener to be removed
+ * @see FileTransferListener
+ */
+ public void removeFileTransferListener(final FileTransferListener li) {
+ if (listeners == null) {
+ return;
+ }
+ synchronized (this.listeners) {
+ listeners.remove(li);
+ }
+ }
+
+ /**
+ * Creates an OutgoingFileTransfer to send a file to another user.
+ *
+ * @param userID
+ * The fully qualified jabber ID with resource of the user to
+ * send the file to.
+ * @return The send file object on which the negotiated transfer can be run.
+ */
+ public OutgoingFileTransfer createOutgoingFileTransfer(String userID) {
+ if (userID == null || StringUtils.parseName(userID).length() <= 0
+ || StringUtils.parseServer(userID).length() <= 0
+ || StringUtils.parseResource(userID).length() <= 0) {
+ throw new IllegalArgumentException(
+ "The provided user id was not fully qualified");
+ }
+
+ return new OutgoingFileTransfer(connection.getUser(), userID,
+ fileTransferNegotiator.getNextStreamID(),
+ fileTransferNegotiator);
+ }
+
+ /**
+ * When the file transfer request is acceptable, this method should be
+ * invoked. It will create an IncomingFileTransfer which allows the
+ * transmission of the file to procede.
+ *
+ * @param request
+ * The remote request that is being accepted.
+ * @return The IncomingFileTransfer which manages the download of the file
+ * from the transfer initiator.
+ */
+ protected IncomingFileTransfer createIncomingFileTransfer(
+ FileTransferRequest request) {
+ if (request == null) {
+ throw new NullPointerException("RecieveRequest cannot be null");
+ }
+
+ IncomingFileTransfer transfer = new IncomingFileTransfer(request,
+ fileTransferNegotiator);
+ transfer.setFileInfo(request.getFileName(), request.getFileSize());
+
+ return transfer;
+ }
+
+ protected void rejectIncomingFileTransfer(FileTransferRequest request) {
+ StreamInitiation initiation = request.getStreamInitiation();
+
+ IQ rejection = FileTransferNegotiator.createIQ(
+ initiation.getPacketID(), initiation.getFrom(), initiation
+ .getTo(), IQ.Type.ERROR);
+ rejection.setError(new XMPPError(403));
+ connection.sendPacket(rejection);
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
new file mode 100644
index 000000000..1123b7fe7
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
@@ -0,0 +1,438 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+import java.net.URLConnection;
+import java.util.*;
+
+/**
+ * Manages the negotiation of file transfers according to JEP-0096. If a file is
+ * being sent the remote user chooses the type of stream under which the file
+ * will be sent.
+ *
+ * @author Alexander Wenckus
+ * @see <a href=http://www.jabber.org/jeps/jep-0096.html>JEP-0096: File Transfer</a>
+ */
+public class FileTransferNegotiator {
+
+ // Static
+
+ /**
+ * The XMPP namespace of the SOCKS5 bytestream
+ */
+ public static final String BYTE_STREAM = "http://jabber.org/protocol/bytestreams";
+
+ /**
+ * The XMPP namespace of the In-Band bytestream
+ */
+ public static final String INBAND_BYTE_STREAM = "http://jabber.org/protocol/ibb";
+
+ private static final String[] NAMESPACE = {
+ "http://jabber.org/protocol/si/profile/file-transfer",
+ "http://jabber.org/protocol/si", BYTE_STREAM, INBAND_BYTE_STREAM};
+
+ private static final String[] PROTOCOLS = {BYTE_STREAM, INBAND_BYTE_STREAM};
+
+ private static final Map transferObject = new HashMap();
+
+ private static final String STREAM_INIT_PREFIX = "jsi_";
+
+ protected static final String STREAM_DATA_FIELD_NAME = "stream-method";
+
+ private static final Random randomGenerator = new Random();
+
+ public static boolean IBB_ONLY = false;
+
+ /**
+ * Returns the file transfer negotiator related to a particular connection.
+ * When this class is requested on a particular connection the file transfer
+ * service is automatically enabled.
+ *
+ * @param connection The connection for which the transfer manager is desired
+ * @return The IMFileTransferManager
+ */
+ public static FileTransferNegotiator getInstanceFor(
+ final XMPPConnection connection) {
+ if (connection == null) {
+ throw new IllegalArgumentException("Connection cannot be null");
+ }
+ if (!connection.isConnected()) {
+ return null;
+ }
+
+ if (transferObject.containsKey(connection)) {
+ return (FileTransferNegotiator) transferObject.get(connection);
+ }
+ else {
+ FileTransferNegotiator transfer = new FileTransferNegotiator(
+ connection);
+ setServiceEnabled(connection, true);
+ transferObject.put(connection, transfer);
+ return transfer;
+ }
+ }
+
+ /**
+ * Enable the Jabber services related to file transfer on the particular
+ * connection.
+ *
+ * @param connection The connection on which to enable or disable the services.
+ * @param isEnabled True to enable, false to disable.
+ */
+ public static void setServiceEnabled(final XMPPConnection connection,
+ final boolean isEnabled) {
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager
+ .getInstanceFor(connection);
+ for (int i = 0; i < NAMESPACE.length; i++) {
+ if (isEnabled) {
+ manager.addFeature(NAMESPACE[i]);
+ }
+ else {
+ manager.removeFeature(NAMESPACE[i]);
+ }
+ }
+ }
+
+ /**
+ * Checks to see if all file transfer related services are enabled on the
+ * connection.
+ *
+ * @param connection The connection to check
+ * @return True if all related services are enabled, false if they are not.
+ */
+ public static boolean isServiceEnabled(final XMPPConnection connection) {
+ for (int i = 0; i < NAMESPACE.length; i++) {
+ if (!ServiceDiscoveryManager.getInstanceFor(connection)
+ .includesFeature(NAMESPACE[i]))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * A convience method to create an IQ packet.
+ *
+ * @param ID The packet ID of the
+ * @param to To whom the packet is addressed.
+ * @param from From whom the packet is sent.
+ * @param type The iq type of the packet.
+ * @return The created IQ packet.
+ */
+ protected static IQ createIQ(final String ID, final String to,
+ final String from, final IQ.Type type) {
+ IQ iqPacket = new IQ() {
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ iqPacket.setPacketID(ID);
+ iqPacket.setTo(to);
+ iqPacket.setFrom(from);
+ iqPacket.setType(type);
+
+ return iqPacket;
+ }
+
+ /**
+ * Returns a collection of the supported transfer protocols.
+ *
+ * @return Returns a collection of the supported transfer protocols.
+ */
+ public static Collection getSupportedProtocols() {
+ return Collections.unmodifiableList(Arrays.asList(PROTOCOLS));
+ }
+
+ // non-static
+
+ private final XMPPConnection connection;
+
+ private final StreamNegotiator byteStreamTransferManager;
+
+ private final StreamNegotiator inbandTransferManager;
+
+ private FileTransferNegotiator(final XMPPConnection connection) {
+ configureConnection(connection);
+
+ this.connection = connection;
+ byteStreamTransferManager = new Socks5TransferNegotiator(connection);
+ inbandTransferManager = new IBBTransferNegotiator(connection);
+ }
+
+ private void configureConnection(final XMPPConnection connection) {
+ connection.addConnectionListener(new ConnectionListener() {
+ public void connectionClosed() {
+ cleanup(connection);
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ cleanup(connection);
+ }
+ });
+ }
+
+ private void cleanup(final XMPPConnection connection) {
+ transferObject.remove(connection);
+
+ byteStreamTransferManager.cleanup();
+ inbandTransferManager.cleanup();
+ }
+
+ /**
+ * Selects an appropriate stream negotiator after examining the incoming file transfer request.
+ *
+ * @param request The related file transfer request.
+ * @return The file transfer object that handles the transfer
+ * @throws XMPPException If there are either no stream methods contained in the packet, or
+ * there is not an appropriate stream method.
+ */
+ public StreamNegotiator selectStreamNegotiator(
+ FileTransferRequest request) throws XMPPException {
+ StreamInitiation si = request.getStreamInitiation();
+ FormField streamMethodField = getStreamMethodField(si
+ .getFeatureNegotiationForm());
+
+ if (streamMethodField == null) {
+ XMPPError error = new XMPPError(400);
+ IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
+ IQ.Type.ERROR);
+ iqPacket.setError(error);
+ connection.sendPacket(iqPacket);
+ throw new XMPPException("No stream methods contained in packet.", error);
+ }
+
+ // select the appropriate protocol
+
+ StreamNegotiator selectedStreamNegotiator;
+ try {
+ selectedStreamNegotiator = getNegotiator(streamMethodField);
+ }
+ catch (XMPPException e) {
+ IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
+ IQ.Type.ERROR);
+ iqPacket.setError(e.getXMPPError());
+ connection.sendPacket(iqPacket);
+ throw e;
+ }
+
+ // return the appropriate negotiator
+
+ return selectedStreamNegotiator;
+ }
+
+ private FormField getStreamMethodField(DataForm form) {
+ FormField field = null;
+ for (Iterator it = form.getFields(); it.hasNext();) {
+ field = (FormField) it.next();
+ if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) {
+ break;
+ }
+ field = null;
+ }
+ return field;
+ }
+
+ private StreamNegotiator getNegotiator(final FormField field)
+ throws XMPPException {
+ String variable;
+ boolean isByteStream = false;
+ boolean isIBB = false;
+ for (Iterator it = field.getOptions(); it.hasNext();) {
+ variable = ((FormField.Option) it.next()).getValue();
+ if (variable.equals(BYTE_STREAM) && !IBB_ONLY) {
+ isByteStream = true;
+ }
+ else if (variable.equals(INBAND_BYTE_STREAM)) {
+ isIBB = true;
+ }
+ }
+
+ if (!isByteStream && !isIBB) {
+ XMPPError error = new XMPPError(400);
+ throw new XMPPException("No acceptable transfer mechanism", error);
+ }
+
+ if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) {
+ return new FaultTolerantNegotiator(connection, byteStreamTransferManager, inbandTransferManager);
+ }
+ else if (isByteStream) {
+ return byteStreamTransferManager;
+ }
+ else {
+ return inbandTransferManager;
+ }
+ }
+
+ /**
+ * Reject a stream initiation request from a remote user.
+ *
+ * @param si The Stream Initiation request to reject.
+ */
+ public void rejectStream(final StreamInitiation si) {
+ XMPPError error = new XMPPError(403, "Offer Declined");
+ IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
+ IQ.Type.ERROR);
+ iqPacket.setError(error);
+ connection.sendPacket(iqPacket);
+ }
+
+ /**
+ * Returns a new, unique, stream ID to identify a file transfer.
+ *
+ * @return Returns a new, unique, stream ID to identify a file transfer.
+ */
+ public String getNextStreamID() {
+ StringBuffer buffer = new StringBuffer();
+ buffer.append(STREAM_INIT_PREFIX);
+ buffer.append(Math.abs(randomGenerator.nextLong()));
+
+ return buffer.toString();
+ }
+
+ /**
+ * Send a request to another user to send them a file. The other user has
+ * the option of, accepting, rejecting, or not responding to a received file
+ * transfer request.
+ * <p/>
+ * If they accept, the packet will contain the other user's choosen stream
+ * type to send the file across. The two choices this implementation
+ * provides to the other user for file transfer are <a
+ * href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>,
+ * which is the prefered method of transfer, and <a
+ * href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>,
+ * which is the fallback mechanism.
+ * <p/>
+ * The other user may choose to decline the file request if they do not
+ * desire the file, their client does not support JEP-0096, or if there are
+ * no acceptable means to transfer the file.
+ * <p/>
+ * Finally, if the other user does not respond this method will return null
+ * after the specified timeout.
+ *
+ * @param userID The userID of the user to whom the file will be sent.
+ * @param streamID The unique identifier for this file transfer.
+ * @param fileName The name of this file. Preferably it should include an
+ * extension as it is used to determine what type of file it is.
+ * @param size The size, in bytes, of the file.
+ * @param desc A description of the file.
+ * @param responseTimeout The amount of time, in milliseconds, to wait for the remote
+ * user to respond. If they do not respond in time, this
+ * @return Returns the stream negotiator selected by the peer.
+ * @throws XMPPException Thrown if there is an error negotiating the file transfer.
+ */
+ public StreamNegotiator negotiateOutgoingTransfer(final String userID,
+ final String streamID, final String fileName, final long size,
+ final String desc, int responseTimeout) throws XMPPException {
+ StreamInitiation si = new StreamInitiation();
+ si.setSesssionID(streamID);
+ si.setMimeType(URLConnection.guessContentTypeFromName(fileName));
+
+ StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);
+ siFile.setDesc(desc);
+ si.setFile(siFile);
+
+ si.setFeatureNegotiationForm(createDefaultInitiationForm());
+
+ si.setFrom(connection.getUser());
+ si.setTo(userID);
+ si.setType(IQ.Type.SET);
+
+ PacketCollector collector = connection
+ .createPacketCollector(new PacketIDFilter(si.getPacketID()));
+ connection.sendPacket(si);
+ Packet siResponse = collector.nextResult(responseTimeout);
+ collector.cancel();
+
+ if (siResponse instanceof IQ) {
+ IQ iqResponse = (IQ) siResponse;
+ if (iqResponse.getType().equals(IQ.Type.RESULT)) {
+ StreamInitiation response = (StreamInitiation) siResponse;
+ return getOutgoingNegotiator(getStreamMethodField(response
+ .getFeatureNegotiationForm()));
+
+ }
+ else if (iqResponse.getType().equals(IQ.Type.ERROR)) {
+ throw new XMPPException(iqResponse.getError());
+ }
+ else {
+ throw new XMPPException("File transfer response unreadable");
+ }
+ }
+ else {
+ return null;
+ }
+ }
+
+ private StreamNegotiator getOutgoingNegotiator(final FormField field)
+ throws XMPPException {
+ String variable;
+ boolean isByteStream = false;
+ boolean isIBB = false;
+ for (Iterator it = field.getValues(); it.hasNext();) {
+ variable = (it.next().toString());
+ if (variable.equals(BYTE_STREAM) && !IBB_ONLY) {
+ isByteStream = true;
+ }
+ else if (variable.equals(INBAND_BYTE_STREAM)) {
+ isIBB = true;
+ }
+ }
+
+ if (!isByteStream && !isIBB) {
+ XMPPError error = new XMPPError(400);
+ throw new XMPPException("No acceptable transfer mechanism", error);
+ }
+
+ if (isByteStream && isIBB) {
+ return new FaultTolerantNegotiator(connection, byteStreamTransferManager, inbandTransferManager);
+ }
+ else if (isByteStream) {
+ return byteStreamTransferManager;
+ }
+ else {
+ return inbandTransferManager;
+ }
+ }
+
+ private DataForm createDefaultInitiationForm() {
+ DataForm form = new DataForm(Form.TYPE_FORM);
+ FormField field = new FormField(STREAM_DATA_FIELD_NAME);
+ field.setType(FormField.TYPE_LIST_MULTI);
+ if (!IBB_ONLY) {
+ field.addOption(new FormField.Option(BYTE_STREAM));
+ }
+ field.addOption(new FormField.Option(INBAND_BYTE_STREAM));
+ form.addField(field);
+ return form;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java
new file mode 100644
index 000000000..69a073f67
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java
@@ -0,0 +1,138 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+/**
+ * A request to send a file recieved from another user.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class FileTransferRequest {
+ private final StreamInitiation streamInitiation;
+
+ private final FileTransferManager manager;
+
+ /**
+ * A recieve request is constructed from the Stream Initiation request
+ * received from the initator.
+ *
+ * @param manager
+ * The manager handling this file transfer
+ *
+ * @param si
+ * The Stream initiaton recieved from the initiator.
+ */
+ public FileTransferRequest(FileTransferManager manager, StreamInitiation si) {
+ this.streamInitiation = si;
+ this.manager = manager;
+ }
+
+ /**
+ * Returns the name of the file.
+ *
+ * @return Returns the name of the file.
+ */
+ public String getFileName() {
+ return streamInitiation.getFile().getName();
+ }
+
+ /**
+ * Returns the size in bytes of the file.
+ *
+ * @return Returns the size in bytes of the file.
+ */
+ public long getFileSize() {
+ return streamInitiation.getFile().getSize();
+ }
+
+ /**
+ * Returns the description of the file provided by the requestor.
+ *
+ * @return Returns the description of the file provided by the requestor.
+ */
+ public String getDescription() {
+ return streamInitiation.getFile().getDesc();
+ }
+
+ /**
+ * Returns the mime-type of the file.
+ *
+ * @return Returns the mime-type of the file.
+ */
+ public String getMimeType() {
+ return streamInitiation.getMimeType();
+ }
+
+ /**
+ * Returns the fully-qualified jabber ID of the user that requested this
+ * file transfer.
+ *
+ * @return Returns the fully-qualified jabber ID of the user that requested
+ * this file transfer.
+ */
+ public String getRequestor() {
+ return streamInitiation.getFrom();
+ }
+
+ /**
+ * Returns the stream ID that uniquely identifies this file transfer.
+ *
+ * @return Returns the stream ID that uniquely identifies this file
+ * transfer.
+ */
+ public String getStreamID() {
+ return streamInitiation.getSessionID();
+ }
+
+ /**
+ * Returns the stream initiation packet that was sent by the requestor which
+ * contains the parameters of the file transfer being transfer and also the
+ * methods available to transfer the file.
+ *
+ * @return Returns the stream initiation packet that was sent by the
+ * requestor which contains the parameters of the file transfer
+ * being transfer and also the methods available to transfer the
+ * file.
+ */
+ protected StreamInitiation getStreamInitiation() {
+ return streamInitiation;
+ }
+
+ /**
+ * Accepts this file transfer and creates the incoming file transfer.
+ *
+ * @return Returns the <b><i>IncomingFileTransfer</b></i> on which the
+ * file transfer can be carried out.
+ */
+ public IncomingFileTransfer accept() {
+ return manager.createIncomingFileTransfer(this);
+ }
+
+ /**
+ * Rejects the file transfer request.
+ */
+ public void reject() {
+ manager.rejectIncomingFileTransfer(this);
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java
new file mode 100644
index 000000000..53fed24ee
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java
@@ -0,0 +1,457 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.packet.IBBExtensions;
+import org.jivesoftware.smackx.packet.IBBExtensions.Open;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * The in-band bytestream file transfer method, or IBB for short, transfers the
+ * file over the same XML Stream used by XMPP. It is the fall-back mechanism in
+ * case the SOCKS5 bytestream method of transfering files is not available.
+ *
+ * @author Alexander Wenckus
+ * @see <a href="http://www.jabber.org/jeps/jep-0047.html">JEP-0047: In-Band
+ * Bytestreams (IBB)</a>
+ */
+public class IBBTransferNegotiator extends StreamNegotiator {
+
+ protected static final String NAMESPACE = "http://jabber.org/protocol/ibb";
+
+ public static final int DEFAULT_BLOCK_SIZE = 4096;
+
+ private XMPPConnection connection;
+
+ /**
+ * The default constructor for the In-Band Bystream Negotiator.
+ *
+ * @param connection The connection which this negotiator works on.
+ */
+ protected IBBTransferNegotiator(XMPPConnection connection) {
+ this.connection = connection;
+ }
+
+ public PacketFilter getInitiationPacketFilter(String from, String streamID) {
+ return new AndFilter(new FromContainsFilter(
+ from), new IBBOpenSidFilter(streamID));
+ }
+
+ InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException {
+ Open openRequest = (Open) streamInitiation;
+
+ if (openRequest.getType().equals(IQ.Type.ERROR)) {
+ throw new XMPPException(openRequest.getError());
+ }
+
+ PacketFilter dataFilter = new IBBMessageSidFilter(openRequest.getFrom(),
+ openRequest.getSessionID());
+ PacketFilter closeFilter = new AndFilter(new PacketTypeFilter(
+ IBBExtensions.Close.class), new FromMatchesFilter(openRequest
+ .getFrom()));
+
+ InputStream stream = new IBBInputStream(openRequest.getSessionID(),
+ dataFilter, closeFilter);
+
+ initInBandTransfer(openRequest);
+
+ return stream;
+ }
+
+ public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException {
+ Packet openRequest = initiateIncomingStream(connection, initiation);
+ return negotiateIncomingStream(openRequest);
+ }
+
+ /**
+ * Creates and sends the response for the open request.
+ *
+ * @param openRequest The open request recieved from the peer.
+ */
+ private void initInBandTransfer(final Open openRequest) {
+ connection.sendPacket(FileTransferNegotiator.createIQ(openRequest
+ .getPacketID(), openRequest.getFrom(), openRequest.getTo(),
+ IQ.Type.RESULT));
+ }
+
+ public OutputStream createOutgoingStream(String streamID, String initiator,
+ String target) throws XMPPException {
+ Open openIQ = new Open(streamID, DEFAULT_BLOCK_SIZE);
+ openIQ.setTo(target);
+ openIQ.setType(IQ.Type.SET);
+
+ // wait for the result from the peer
+ PacketCollector collector = connection
+ .createPacketCollector(new PacketIDFilter(openIQ.getPacketID()));
+ connection.sendPacket(openIQ);
+
+ IQ openResponse = (IQ) collector.nextResult();
+ collector.cancel();
+
+ if (openResponse == null) {
+ throw new XMPPException("No response from peer");
+ }
+
+ IQ.Type type = openResponse.getType();
+ if (!type.equals(IQ.Type.RESULT)) {
+ if (type.equals(IQ.Type.ERROR)) {
+ throw new XMPPException("Target returned an error",
+ openResponse.getError());
+ }
+ else {
+ throw new XMPPException("Target returned unknown response");
+ }
+ }
+
+ return new IBBOutputStream(target, streamID, DEFAULT_BLOCK_SIZE);
+ }
+
+ public String[] getNamespaces() {
+ return new String[]{NAMESPACE};
+ }
+
+ public void cleanup() {
+ }
+
+ private class IBBOutputStream extends OutputStream {
+
+ protected byte[] buffer;
+
+ protected int count = 0;
+
+ protected int seq = 0;
+
+ final String userID;
+
+ private final int options = Base64.DONT_BREAK_LINES;
+
+ final private IQ closePacket;
+
+ private String messageID;
+ private String sid;
+
+ IBBOutputStream(String userID, String sid, int blockSize) {
+ if (blockSize <= 0) {
+ throw new IllegalArgumentException("Buffer size <= 0");
+ }
+ buffer = new byte[blockSize];
+ this.userID = userID;
+
+ Message template = new Message(userID);
+ messageID = template.getPacketID();
+ this.sid = sid;
+ closePacket = createClosePacket(userID, sid);
+ }
+
+ private IQ createClosePacket(String userID, String sid) {
+ IQ packet = new IBBExtensions.Close(sid);
+ packet.setTo(userID);
+ packet.setType(IQ.Type.SET);
+ return packet;
+ }
+
+ public void write(int b) throws IOException {
+ if (count >= buffer.length) {
+ flushBuffer();
+ }
+
+ buffer[count++] = (byte) b;
+ }
+
+ public synchronized void write(byte b[], int off, int len)
+ throws IOException {
+ if (len >= buffer.length) {
+ throw new IllegalArgumentException(
+ "byte size exceeds blocksize");
+ }
+ if (len > buffer.length - count) {
+ flushBuffer();
+ }
+ System.arraycopy(b, off, buffer, count, len);
+ count += len;
+ }
+
+ private void flushBuffer() {
+ writeToXML(buffer, 0, count);
+
+ count = 0;
+ }
+
+ private synchronized void writeToXML(byte[] buffer, int offset, int len) {
+ Message template = createTemplate(messageID + "_" + seq);
+ IBBExtensions.Data ext = new IBBExtensions.Data(sid);
+ template.addExtension(ext);
+
+ String enc = Base64.encodeBytes(buffer, offset, len, options);
+
+ ext.setData(enc);
+ ext.setSeq(seq);
+ synchronized(this) {
+ try {
+ this.wait(100);
+ }
+ catch (InterruptedException e) {
+ }
+ }
+
+ connection.sendPacket(template);
+
+ seq = (seq + 1 == 65535 ? 0 : seq + 1);
+ }
+
+ public void close() throws IOException {
+ connection.sendPacket(closePacket);
+ }
+
+ public void flush() throws IOException {
+ flushBuffer();
+ }
+
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ public Message createTemplate(String messageID) {
+ Message template = new Message(userID);
+ template.setPacketID(messageID);
+ return template;
+ }
+ }
+
+ private class IBBInputStream extends InputStream implements PacketListener {
+
+ private String streamID;
+
+ private PacketCollector dataCollector;
+
+ private byte[] buffer;
+
+ private int bufferPointer;
+
+ private int seq = -1;
+
+ private boolean isDone;
+
+ private boolean isEOF;
+
+ private boolean isClosed;
+
+ private IQ closeConfirmation;
+
+ private Message lastMess;
+
+ private IBBInputStream(String streamID, PacketFilter dataFilter,
+ PacketFilter closeFilter) {
+ this.streamID = streamID;
+ this.dataCollector = connection.createPacketCollector(dataFilter);
+ connection.addPacketListener(this, closeFilter);
+ this.bufferPointer = -1;
+ }
+
+ public synchronized int read() throws IOException {
+ if (isEOF || isClosed) {
+ return -1;
+ }
+ if (bufferPointer == -1 || bufferPointer >= buffer.length) {
+ loadBufferWait();
+ }
+
+ return (int) buffer[bufferPointer++];
+ }
+
+ public synchronized int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ public synchronized int read(byte[] b, int off, int len)
+ throws IOException {
+ if (isEOF || isClosed) {
+ return -1;
+ }
+ if (bufferPointer == -1 || bufferPointer >= buffer.length) {
+ if (!loadBufferWait()) {
+ isEOF = true;
+ return -1;
+ }
+ }
+
+ if (len - off > buffer.length - bufferPointer) {
+ len = buffer.length - bufferPointer;
+ }
+
+ System.arraycopy(buffer, bufferPointer, b, off, len);
+ bufferPointer += len;
+ return len;
+ }
+
+ private boolean loadBufferWait() throws IOException {
+ IBBExtensions.Data data;
+
+ Message mess = null;
+ while (mess == null) {
+ if (isDone) {
+ mess = (Message) dataCollector.pollResult();
+ if (mess == null) {
+ return false;
+ }
+ }
+ else {
+ mess = (Message) dataCollector.nextResult(1000);
+ }
+ }
+ lastMess = mess;
+ data = (IBBExtensions.Data) mess.getExtension(
+ IBBExtensions.Data.ELEMENT_NAME,
+ IBBExtensions.NAMESPACE);
+
+ checkSequence(mess, (int) data.getSeq());
+ buffer = Base64.decode(data.getData());
+ bufferPointer = 0;
+ return true;
+ }
+
+ private void checkSequence(Message mess, int seq) throws IOException {
+ if (this.seq == 65535) {
+ this.seq = -1;
+ }
+ if (seq - 1 != this.seq) {
+ cancelTransfer(mess);
+ throw new IOException("Packets out of sequence");
+ }
+ else {
+ this.seq = seq;
+ }
+ }
+
+ private void cancelTransfer(Message mess) {
+ cleanup();
+
+ sendCancelMessage(mess);
+ }
+
+ private void cleanup() {
+ dataCollector.cancel();
+ connection.removePacketListener(this);
+ }
+
+ private void sendCancelMessage(Message message) {
+ IQ error = FileTransferNegotiator.createIQ(message.getPacketID(), message.getFrom(), message.getTo(),
+ IQ.Type.ERROR);
+ error.setError(new XMPPError(504));
+ connection.sendPacket(error);
+ }
+
+ public boolean markSupported() {
+ return false;
+ }
+
+ public void processPacket(Packet packet) {
+ IBBExtensions.Close close = (IBBExtensions.Close) packet;
+ if (close.getSessionID().equals(streamID)) {
+ isDone = true;
+ closeConfirmation = FileTransferNegotiator.createIQ(packet
+ .getPacketID(), packet.getFrom(), packet.getTo(),
+ IQ.Type.RESULT);
+ }
+ }
+
+ public synchronized void close() throws IOException {
+ if (isClosed) {
+ return;
+ }
+ cleanup();
+
+ if (isEOF) {
+ sendCloseConfirmation();
+ }
+ else if(lastMess != null) {
+ sendCancelMessage(lastMess);
+ }
+ isClosed = true;
+ }
+
+ private void sendCloseConfirmation() {
+ connection.sendPacket(closeConfirmation);
+ }
+ }
+
+ private static class IBBOpenSidFilter implements PacketFilter {
+
+ private String sessionID;
+
+ public IBBOpenSidFilter(String sessionID) {
+ if (sessionID == null) {
+ throw new IllegalArgumentException("StreamID cannot be null");
+ }
+ this.sessionID = sessionID;
+ }
+
+ public boolean accept(Packet packet) {
+ if (!IBBExtensions.Open.class.isInstance(packet)) {
+ return false;
+ }
+ IBBExtensions.Open open = (IBBExtensions.Open) packet;
+ String sessionID = open.getSessionID();
+
+ return (sessionID != null && sessionID.equals(this.sessionID));
+ }
+ }
+
+ private static class IBBMessageSidFilter implements PacketFilter {
+
+ private final String sessionID;
+ private String from;
+
+ public IBBMessageSidFilter(String from, String sessionID) {
+ this.from = from;
+ this.sessionID = sessionID;
+ }
+
+ public boolean accept(Packet packet) {
+ if (!(packet instanceof Message)) {
+ return false;
+ }
+ if (!packet.getFrom().equalsIgnoreCase(from)) {
+ return false;
+ }
+
+ IBBExtensions.Data data = (IBBExtensions.Data) packet.
+ getExtension(IBBExtensions.Data.ELEMENT_NAME, IBBExtensions.NAMESPACE);
+ if (data == null) {
+ return false;
+ }
+ return data.getSessionID() != null && data.getSessionID().equalsIgnoreCase(sessionID);
+ }
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java
new file mode 100644
index 000000000..33be034c2
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java
@@ -0,0 +1,187 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import org.jivesoftware.smack.XMPPException;
+
+import java.io.*;
+
+/**
+ * An incoming file transfer is created when the
+ * {@link FileTransferManager#createIncomingFileTransfer(FileTransferRequest)}
+ * method is invoked. It is a file being sent to the local user from another
+ * user on the jabber network. There are two stages of the file transfer to be
+ * concerned with and they can be handled in different ways depending upon the
+ * method that is invoked on this class.
+ * <p/>
+ * The first way that a file is recieved is by calling the
+ * {@link #recieveFile()} method. This method, negotiates the appropriate stream
+ * method and then returns the <b><i>InputStream</b></i> to read the file
+ * data from.
+ * <p/>
+ * The second way that a file can be recieved through this class is by invoking
+ * the {@link #recieveFile(File)} method. This method returns immediatly and
+ * takes as its parameter a file on the local file system where the file
+ * recieved from the transfer will be put.
+ *
+ * @author Alexander Wenckus
+ */
+public class IncomingFileTransfer extends FileTransfer {
+
+ private FileTransferRequest recieveRequest;
+
+ private Thread transferThread;
+
+ private InputStream inputStream;
+
+ protected IncomingFileTransfer(FileTransferRequest request,
+ FileTransferNegotiator transferNegotiator) {
+ super(request.getRequestor(), request.getStreamID(), transferNegotiator);
+ this.recieveRequest = request;
+ }
+
+ /**
+ * Negotiates the stream method to transfer the file over and then returns
+ * the negotiated stream.
+ *
+ * @return The negotiated InputStream from which to read the data.
+ * @throws XMPPException If there is an error in the negotiation process an exception
+ * is thrown.
+ */
+ public InputStream recieveFile() throws XMPPException {
+ if (inputStream != null) {
+ throw new IllegalStateException("Transfer already negotiated!");
+ }
+
+ try {
+ inputStream = negotiateStream();
+ }
+ catch (XMPPException e) {
+ setException(e);
+ throw e;
+ }
+
+ return inputStream;
+ }
+
+ /**
+ * This method negotitates the stream and then transfer's the file over the
+ * negotiated stream. The transfered file will be saved at the provided
+ * location.
+ * <p/>
+ * This method will return immedialtly, file transfer progress can be
+ * monitored through several methods:
+ * <p/>
+ * <UL>
+ * <LI>{@link FileTransfer#getStatus()}
+ * <LI>{@link FileTransfer#getProgress()}
+ * <LI>{@link FileTransfer#isDone()}
+ * </UL>
+ *
+ * @param file The location to save the file.
+ * @throws XMPPException
+ * @throws IllegalArgumentException This exception is thrown when the the provided file is
+ * either null, or cannot be written to.
+ */
+ public void recieveFile(final File file) throws XMPPException {
+ if (file != null) {
+ if (!file.exists()) {
+ try {
+ file.createNewFile();
+ }
+ catch (IOException e) {
+ throw new XMPPException(
+ "Could not create file to write too", e);
+ }
+ }
+ if (!file.canWrite()) {
+ throw new IllegalArgumentException("Cannot write to provided file");
+ }
+ }
+ else {
+ throw new IllegalArgumentException("File cannot be null");
+ }
+
+ transferThread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ inputStream = negotiateStream();
+ }
+ catch (XMPPException e) {
+ handleXMPPException(e);
+ return;
+ }
+
+ OutputStream outputStream = null;
+ try {
+ outputStream = new FileOutputStream(file);
+ setStatus(Status.IN_PROGRESS);
+ writeToStream(inputStream, outputStream);
+ }
+ catch (XMPPException e) {
+ setStatus(FileTransfer.Status.ERROR);
+ setError(Error.STREAM);
+ setException(e);
+ }
+ catch (FileNotFoundException e) {
+ setStatus(FileTransfer.Status.ERROR);
+ setError(Error.BAD_FILE);
+ setException(e);
+ }
+
+ if (getStatus().equals(Status.IN_PROGRESS))
+ setStatus(Status.COMPLETE);
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ }
+ catch (IOException e) {
+ }
+ }
+ }, "File Transfer " + streamID);
+ transferThread.start();
+
+ }
+
+ private void handleXMPPException(XMPPException e) {
+ setStatus(FileTransfer.Status.ERROR);
+ setException(e);
+ }
+
+ private InputStream negotiateStream() throws XMPPException {
+ setStatus(Status.NEGOTIATING_TRANSFER);
+ StreamNegotiator streamNegotiator = negotiator
+ .selectStreamNegotiator(recieveRequest);
+ setStatus(Status.NEGOTIATING_STREAM);
+ InputStream inputStream = streamNegotiator
+ .createIncomingStream(recieveRequest.getStreamInitiation());
+ setStatus(Status.NEGOTIATED);
+ return inputStream;
+ }
+
+ public void cancel() {
+ setStatus(Status.CANCLED);
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java
new file mode 100644
index 000000000..db0f1c034
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java
@@ -0,0 +1,364 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.XMPPError;
+
+/**
+ * Handles the sending of a file to another user. File transfer's in jabber have
+ * several steps and there are several methods in this class that handle these
+ * steps differently.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class OutgoingFileTransfer extends FileTransfer {
+
+ private static int RESPONSE_TIMEOUT = 60 * 1000;
+
+ /**
+ * Returns the time in milliseconds after which the file transfer
+ * negotiation process will timeout if the other user has not responded.
+ *
+ * @return Returns the time in milliseconds after which the file transfer
+ * negotiation process will timeout if the remote user has not
+ * responded.
+ */
+ public static int getResponseTimeout() {
+ return RESPONSE_TIMEOUT;
+ }
+
+ /**
+ * Sets the time in milliseconds after which the file transfer negotiation
+ * process will timeout if the other user has not responded.
+ *
+ * @param responseTimeout
+ * The timeout time in milliseconds.
+ */
+ public void setResponseTimeout(int responseTimeout) {
+ RESPONSE_TIMEOUT = responseTimeout;
+ }
+
+ private OutputStream outputStream;
+
+ private String initiator;
+
+ private Thread transferThread;
+
+ protected OutgoingFileTransfer(String initiator, String target,
+ String streamID, FileTransferNegotiator transferNegotiator) {
+ super(target, streamID, transferNegotiator);
+ this.initiator = initiator;
+ }
+
+ protected void setOutputStream(OutputStream stream) {
+ if (outputStream == null) {
+ this.outputStream = stream;
+ }
+ }
+
+ /**
+ * Returns the output stream connected to the peer to transfer the file. It
+ * is only available after it has been succesfully negotiated by the
+ * {@link StreamNegotiator}.
+ *
+ * @return Returns the output stream connected to the peer to transfer the
+ * file.
+ */
+ protected OutputStream getOutputStream() {
+ if (getStatus().equals(FileTransfer.Status.NEGOTIATED)) {
+ return outputStream;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * This method handles the negotiation of the file transfer and the stream,
+ * it only returns the created stream after the negotiation has been completed.
+ *
+ * @param fileName
+ * The name of the file that will be transmitted. It is
+ * preferable for this name to have an extension as it will be
+ * used to determine the type of file it is.
+ * @param fileSize
+ * The size in bytes of the file that will be transmitted.
+ * @param description
+ * A description of the file that will be transmitted.
+ * @return The OutputStream that is connected to the peer to transmit the
+ * file.
+ * @throws XMPPException
+ * Thrown if an error occurs during the file transfer
+ * negotiation process.
+ */
+ public synchronized OutputStream sendFile(String fileName, long fileSize,
+ String description) throws XMPPException {
+ if (isDone() || outputStream != null) {
+ throw new IllegalStateException(
+ "The negotation process has already"
+ + " been attempted on this file transfer");
+ }
+ try {
+ this.outputStream = negotiateStream(fileName, fileSize, description);
+ } catch (XMPPException e) {
+ handleXMPPException(e);
+ throw e;
+ }
+ return outputStream;
+ }
+
+ /**
+ * This methods handles the transfer and stream negotiation process. It
+ * returns immediately and its progress can be monitored through the
+ * {@link NegotiationProgress} callback. When the negotiation process is
+ * complete the OutputStream can be retrieved from the callback via the
+ * {@link NegotiationProgress#getOutputStream()} method.
+ *
+ * @param fileName
+ * The name of the file that will be transmitted. It is
+ * preferable for this name to have an extension as it will be
+ * used to determine the type of file it is.
+ * @param fileSize
+ * The size in bytes of the file that will be transmitted.
+ * @param description
+ * A description of the file that will be transmitted.
+ * @param progress
+ * A callback to monitor the progress of the file transfer
+ * negotiation process and to retrieve the OutputStream when it
+ * is complete.
+ */
+ public synchronized void sendFile(final String fileName,
+ final long fileSize, final String description,
+ NegotiationProgress progress) {
+ checkTransferThread();
+ if (isDone() || outputStream != null) {
+ throw new IllegalStateException(
+ "The negotation process has already"
+ + " been attempted for this file transfer");
+ }
+ progress.delegate = this;
+ transferThread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ OutgoingFileTransfer.this.outputStream = negotiateStream(
+ fileName, fileSize, description);
+ } catch (XMPPException e) {
+ handleXMPPException(e);
+ }
+ }
+ }, "File Transfer Negotiation " + streamID);
+ transferThread.start();
+ }
+
+ private void checkTransferThread() {
+ if (transferThread != null && transferThread.isAlive() || isDone()) {
+ throw new IllegalStateException(
+ "File transfer in progress or has already completed.");
+ }
+ }
+
+ /**
+ * This method handles the stream negotiation process and transmits the file
+ * to the remote user. It returns immediatly and the progress of the file
+ * transfer can be monitored through several methods:
+ *
+ * <UL>
+ * <LI>{@link FileTransfer#getStatus()}
+ * <LI>{@link FileTransfer#getProgress()}
+ * <LI>{@link FileTransfer#isDone()}
+ * </UL>
+ *
+ * @throws XMPPException
+ * If there is an error during the negotiation process or the
+ * sending of the file.
+ */
+ public synchronized void sendFile(final File file, final String description)
+ throws XMPPException {
+ checkTransferThread();
+ if (file == null || !file.exists() || !file.canRead()) {
+ throw new IllegalArgumentException("Could not read file");
+ } else {
+ setFileInfo(file.getAbsolutePath(), file.getName(), file.length());
+ }
+
+ transferThread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ outputStream = negotiateStream(file.getName(), file
+ .length(), description);
+ } catch (XMPPException e) {
+ handleXMPPException(e);
+ return;
+ }
+ if (outputStream == null) {
+ return;
+ }
+
+ if (!getStatus().equals(Status.NEGOTIATED)) {
+ return;
+ }
+ setStatus(Status.IN_PROGRESS);
+
+ InputStream inputStream = null;
+ try {
+ inputStream = new FileInputStream(file);
+ writeToStream(inputStream, outputStream);
+ } catch (FileNotFoundException e) {
+ setStatus(FileTransfer.Status.ERROR);
+ setError(Error.BAD_FILE);
+ setException(e);
+ } catch (XMPPException e) {
+ setStatus(FileTransfer.Status.ERROR);
+ setException(e);
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+
+ outputStream.flush();
+ outputStream.close();
+ } catch (IOException e) {
+ }
+ }
+ if (getStatus().equals(Status.IN_PROGRESS)) {
+ setStatus(FileTransfer.Status.COMPLETE);
+ }
+ }
+
+ }, "File Transfer " + streamID);
+ transferThread.start();
+ }
+
+ private void handleXMPPException(XMPPException e) {
+ setStatus(FileTransfer.Status.ERROR);
+ XMPPError error = e.getXMPPError();
+ if (error != null) {
+ int code = error.getCode();
+ if (code == 403) {
+ setStatus(Status.REFUSED);
+ return;
+ } else if (code == 400) {
+ setStatus(Status.ERROR);
+ setError(Error.NOT_ACCEPTABLE);
+ }
+ }
+ setException(e);
+ return;
+ }
+
+ /**
+ * Returns the amount of bytes that have been sent for the file transfer. Or
+ * -1 if the file transfer has not started.
+ * <p>
+ * Note: This method is only useful when the {@link #sendFile(File, String)}
+ * method is called, as it is the only method that actualy transmits the
+ * file.
+ *
+ * @return Returns the amount of bytes that have been sent for the file
+ * transfer. Or -1 if the file transfer has not started.
+ */
+ public long getBytesSent() {
+ return amountWritten;
+ }
+
+ private OutputStream negotiateStream(String fileName, long fileSize,
+ String description) throws XMPPException {
+ // Negotiate the file transfer profile
+
+ setStatus(Status.NEGOTIATING_TRANSFER);
+ StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer(
+ getPeer(), streamID, fileName, fileSize, description,
+ RESPONSE_TIMEOUT);
+
+ if (streamNegotiator == null) {
+ setStatus(Status.ERROR);
+ setError(Error.NO_RESPONSE);
+ return null;
+ }
+
+ if (!getStatus().equals(Status.NEGOTIATING_TRANSFER)) {
+ return null;
+ }
+
+ // Negotiate the stream
+
+ setStatus(Status.NEGOTIATING_STREAM);
+ outputStream = streamNegotiator.createOutgoingStream(streamID,
+ initiator, getPeer());
+ if (!getStatus().equals(Status.NEGOTIATING_STREAM)) {
+ return null;
+ }
+ setStatus(Status.NEGOTIATED);
+ return outputStream;
+ }
+
+ public void cancel() {
+ setStatus(Status.CANCLED);
+ }
+
+ /**
+ * A callback class to retrive the status of an outgoing transfer
+ * negotiation process.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public static class NegotiationProgress {
+
+ private OutgoingFileTransfer delegate;
+
+ /**
+ * Returns the current status of the negotiation process.
+ *
+ * @return Returns the current status of the negotiation process.
+ */
+ public Status getStatus() {
+ if (delegate == null) {
+ throw new IllegalStateException("delegate not yet set");
+ }
+ return delegate.getStatus();
+ }
+
+ /**
+ * Once the negotiation process is completed the output stream can be
+ * retrieved.
+ *
+ * @return Once the negotiation process is completed the output stream
+ * can be retrieved.
+ *
+ */
+ public OutputStream getOutputStream() {
+ if (delegate == null) {
+ throw new IllegalStateException("delegate not yet set");
+ }
+ return delegate.getOutputStream();
+ }
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java
new file mode 100644
index 000000000..1252fb3c7
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java
@@ -0,0 +1,777 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.FromMatchesFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.Bytestream;
+import org.jivesoftware.smackx.packet.Bytestream.StreamHost;
+import org.jivesoftware.smackx.packet.Bytestream.StreamHostUsed;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.DiscoverItems.Item;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+/**
+ * A SOCKS5 bytestream is negotiated partly over the XMPP XML stream and partly
+ * over a seperate socket. The actual transfer though takes place over a
+ * seperatly created socket.
+ * <p/>
+ * A SOCKS5 file transfer generally has three parites, the initiator, the
+ * target, and the stream host. The stream host is a specialized SOCKS5 proxy
+ * setup on the server, or, the Initiator can act as the Stream Host if the
+ * proxy is not available.
+ * <p/>
+ * The advantage of having a seperate proxy over directly connecting to
+ * eachother is if the Initator and the Target are not on the same LAN and are
+ * operating behind NAT, the proxy allows for a common location for both parties
+ * to connect to and transfer the file.
+ * <p/>
+ * Smack will attempt to automatically discover any proxies present on your
+ * server. If any are detected they will be forwarded to any user attempting to
+ * recieve files from you.
+ *
+ * @author Alexander Wenckus
+ * @see <a href="http://www.jabber.org/jeps/jep-0065.html">JEP-0065: SOCKS5
+ * Bytestreams</a>
+ */
+public class Socks5TransferNegotiator extends StreamNegotiator {
+
+ protected static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
+
+ public static boolean isAllowLocalProxyHost = true;
+
+ private final XMPPConnection connection;
+
+ private List proxies;
+
+ private List streamHosts;
+
+ // locks the proxies during their initialization process
+ private final Object proxyLock = new Object();
+
+ private ProxyProcess proxyProcess;
+
+ // locks on the proxy process during its initiatilization process
+ private final Object processLock = new Object();
+
+ public Socks5TransferNegotiator(final XMPPConnection connection) {
+ this.connection = connection;
+ }
+
+ public PacketFilter getInitiationPacketFilter(String from, String sessionID) {
+ return new AndFilter(new FromMatchesFilter(from),
+ new BytestreamSIDFilter(sessionID));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload(org.jivesoftware.smackx.packet.StreamInitiation,
+ * java.io.File)
+ */
+ InputStream negotiateIncomingStream(Packet streamInitiation)
+ throws XMPPException {
+
+ Bytestream streamHostsInfo = (Bytestream) streamInitiation;
+
+ if (streamHostsInfo.getType().equals(IQ.Type.ERROR)) {
+ throw new XMPPException(streamHostsInfo.getError());
+ }
+ SelectedHostInfo selectedHost;
+ try {
+ // select appropriate host
+ selectedHost = selectHost(streamHostsInfo);
+ }
+ catch (XMPPException ex) {
+ if (ex.getXMPPError() != null) {
+ IQ errorPacket = super.createError(streamHostsInfo.getTo(),
+ streamHostsInfo.getFrom(), streamHostsInfo.getPacketID(),
+ ex.getXMPPError());
+ connection.sendPacket(errorPacket);
+ }
+ throw(ex);
+ }
+
+ // send used-host confirmation
+ Bytestream streamResponse = createUsedHostConfirmation(
+ selectedHost.selectedHost, streamHostsInfo.getFrom(),
+ streamHostsInfo.getTo(), streamHostsInfo.getPacketID());
+ connection.sendPacket(streamResponse);
+
+ try {
+ return selectedHost.establishedSocket.getInputStream();
+ }
+ catch (IOException e) {
+ throw new XMPPException("Error establishing input stream", e);
+ }
+
+ }
+
+ public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException {
+ Packet streamInitiation = initiateIncomingStream(connection, initiation);
+ return negotiateIncomingStream(streamInitiation);
+ }
+
+ /**
+ * The used host confirmation is sent to the initiator to indicate to them
+ * which of the hosts they provided has been selected and successfully
+ * connected to.
+ *
+ * @param selectedHost The selected stream host.
+ * @param initiator The initiator of the stream.
+ * @param target The target of the stream.
+ * @param packetID The of the packet being responded to.
+ * @return The packet that was created to send to the initiator.
+ */
+ private Bytestream createUsedHostConfirmation(StreamHost selectedHost,
+ String initiator, String target, String packetID) {
+ Bytestream streamResponse = new Bytestream();
+ streamResponse.setTo(initiator);
+ streamResponse.setFrom(target);
+ streamResponse.setType(IQ.Type.RESULT);
+ streamResponse.setPacketID(packetID);
+ streamResponse.setUsedHost(selectedHost.getJID());
+ return streamResponse;
+ }
+
+ /**
+ * @param streamHostsInfo
+ * @return
+ * @throws XMPPException
+ */
+ private SelectedHostInfo selectHost(Bytestream streamHostsInfo)
+ throws XMPPException {
+ Iterator it = streamHostsInfo.getStreamHosts().iterator();
+ StreamHost selectedHost = null;
+ Socket socket = null;
+ while (it.hasNext()) {
+ selectedHost = (StreamHost) it.next();
+
+ // establish socket
+ try {
+ socket = new Socket(selectedHost.getAddress(), selectedHost
+ .getPort());
+ establishSOCKS5ConnectionToProxy(socket, createDigest(
+ streamHostsInfo.getSessionID(), streamHostsInfo
+ .getFrom(), streamHostsInfo.getTo()));
+ break;
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ selectedHost = null;
+ socket = null;
+ }
+ }
+ if (selectedHost == null || socket == null) {
+ throw new XMPPException(
+ "Could not establish socket with any provided host", new XMPPError(406));
+ }
+
+ return new SelectedHostInfo(selectedHost, socket);
+ }
+
+ /**
+ * Creates the digest needed for a byte stream. It is the SHA1(sessionID +
+ * initiator + target).
+ *
+ * @param sessionID The sessionID of the stream negotiation
+ * @param initiator The inititator of the stream negotiation
+ * @param target The target of the stream negotiation
+ * @return SHA-1 hash of the three parameters
+ */
+ private String createDigest(final String sessionID, final String initiator,
+ final String target) {
+ return StringUtils.hash(sessionID + StringUtils.parseName(initiator)
+ + "@" + StringUtils.parseServer(initiator) + "/"
+ + StringUtils.parseResource(initiator)
+ + StringUtils.parseName(target) + "@"
+ + StringUtils.parseServer(target) + "/"
+ + StringUtils.parseResource(target));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateUpload(java.lang.String,
+ * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File)
+ */
+ public OutputStream createOutgoingStream(String streamID, String initiator,
+ String target) throws XMPPException {
+ Socket socket;
+ try {
+ socket = initBytestreamSocket(streamID, initiator, target);
+ }
+ catch (Exception e) {
+ throw new XMPPException("Error establishing transfer socket", e);
+ }
+
+ if (socket != null) {
+ try {
+ return socket.getOutputStream();
+ }
+ catch (IOException e) {
+ throw new XMPPException("Error establishing output stream", e);
+ }
+ }
+ return null;
+ }
+
+ private Socket initBytestreamSocket(final String sessionID,
+ String initiator, String target) throws Exception {
+ ProxyProcess process;
+ try {
+ process = establishListeningSocket();
+ }
+ catch (IOException io) {
+ process = null;
+ }
+
+ String localIP;
+ try {
+ localIP = discoverLocalIP();
+ }
+ catch (UnknownHostException e1) {
+ localIP = null;
+ }
+
+ Bytestream query = createByteStreamInit(initiator, target, sessionID,
+ localIP, (process != null ? process.getPort() : 0));
+
+ // if the local host is one of the options we need to wait for the
+ // remote connection.
+ Socket conn = waitForUsedHostResponse(sessionID, process, createDigest(
+ sessionID, initiator, target), query).establishedSocket;
+ cleanupListeningSocket();
+ return conn;
+ }
+
+
+ /**
+ * Waits for the peer to respond with which host they chose to use.
+ *
+ * @param sessionID The session id of the stream.
+ * @param proxy The server socket which will listen locally for remote
+ * connections.
+ * @param digest
+ * @param query
+ * @return
+ * @throws XMPPException
+ * @throws IOException
+ */
+ private SelectedHostInfo waitForUsedHostResponse(String sessionID,
+ final ProxyProcess proxy, final String digest,
+ final Bytestream query) throws XMPPException, IOException {
+ SelectedHostInfo info = new SelectedHostInfo();
+
+ PacketCollector collector = connection
+ .createPacketCollector(new PacketIDFilter(query.getPacketID()));
+ connection.sendPacket(query);
+
+ Packet packet = collector.nextResult();
+ collector.cancel();
+ Bytestream response;
+ if (packet instanceof Bytestream) {
+ response = (Bytestream) packet;
+ }
+ else {
+ throw new XMPPException("Unexpected response from remote user");
+ }
+
+ // check for an error
+ if (response.getType().equals(IQ.Type.ERROR)) {
+ throw new XMPPException("Remote client returned error, stream hosts expected",
+ response.getError());
+ }
+
+ StreamHostUsed used = response.getUsedHost();
+ StreamHost usedHost = query.getStreamHost(used.getJID());
+ if (usedHost == null) {
+ throw new XMPPException("Remote user responded with unknown host");
+ }
+ // The local computer is acting as the proxy
+ if (used.getJID().equals(query.getFrom())) {
+ info.establishedSocket = proxy.getSocket(digest);
+ info.selectedHost = usedHost;
+ return info;
+ }
+ else {
+ info.establishedSocket = new Socket(usedHost.getAddress(), usedHost
+ .getPort());
+ establishSOCKS5ConnectionToProxy(info.establishedSocket, digest);
+
+ Bytestream activate = createByteStreamActivate(sessionID, response
+ .getTo(), usedHost.getJID(), response.getFrom());
+
+ collector = connection.createPacketCollector(new PacketIDFilter(
+ activate.getPacketID()));
+ connection.sendPacket(activate);
+
+ IQ serverResponse = (IQ) collector.nextResult();
+ collector.cancel();
+ if (!serverResponse.getType().equals(IQ.Type.RESULT)) {
+ info.establishedSocket.close();
+ return null;
+ }
+ return info;
+ }
+ }
+
+ private ProxyProcess establishListeningSocket() throws IOException {
+ synchronized (processLock) {
+ if (proxyProcess == null) {
+ proxyProcess = new ProxyProcess(new ServerSocket(7777));
+ proxyProcess.start();
+ }
+ }
+ proxyProcess.addTransfer();
+ return proxyProcess;
+ }
+
+ private void cleanupListeningSocket() {
+ if (proxyProcess == null) {
+ return;
+ }
+ proxyProcess.removeTransfer();
+ }
+
+ private String discoverLocalIP() throws UnknownHostException {
+ return InetAddress.getLocalHost().getHostAddress();
+ }
+
+ /**
+ * The bytestream init looks like this:
+ * <p/>
+ * <pre>
+ * &lt;iq type='set'
+ * from='initiator@host1/foo'
+ * to='target@host2/bar'
+ * id='initiate'&gt;
+ * &lt;query xmlns='http://jabber.org/protocol/bytestreams'
+ * sid='mySID'
+ * mode='tcp'&gt;
+ * &lt;streamhost
+ * jid='initiator@host1/foo'
+ * host='192.168.4.1'
+ * port='5086'/&gt;
+ * &lt;streamhost
+ * jid='proxy.host3'
+ * host='24.24.24.1'
+ * zeroconf='_jabber.bytestreams'/&gt;
+ * &lt;/query&gt;
+ * &lt;/iq&gt;
+ * </pre>
+ *
+ * @param from initiator@host1/foo - The file transfer initiator.
+ * @param to target@host2/bar - The file transfer target.
+ * @param sid 'mySID' - the unique identifier for this file transfer
+ * @param localIP The IP of the local machine if it is being provided, null otherwise.
+ * @param port The port of the local mahine if it is being provided, null otherwise.
+ * @return Returns the created <b><i>Bytestream</b></i> packet
+ */
+ private Bytestream createByteStreamInit(final String from, final String to,
+ final String sid, final String localIP, final int port) {
+ Bytestream bs = new Bytestream();
+ bs.setTo(to);
+ bs.setFrom(from);
+ bs.setSessionID(sid);
+ bs.setType(IQ.Type.SET);
+ bs.setMode(Bytestream.Mode.TCP);
+ if (localIP != null && port > 0) {
+ bs.addStreamHost(from, localIP, port);
+ }
+ // make sure the proxies have been initialized completely
+ synchronized (proxyLock) {
+ if (proxies == null) {
+ initProxies();
+ }
+ }
+ if (streamHosts != null) {
+ Iterator it = streamHosts.iterator();
+ while (it.hasNext()) {
+ bs.addStreamHost((StreamHost) it.next());
+ }
+ }
+
+ return bs;
+ }
+
+ private void initProxies() {
+ proxies = new ArrayList();
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager
+ .getInstanceFor(connection);
+
+ DiscoverItems discoItems;
+ try {
+ discoItems = manager.discoverItems(connection.getServiceName());
+
+ DiscoverItems.Item item;
+ DiscoverInfo info;
+ DiscoverInfo.Identity identity;
+
+ Iterator it = discoItems.getItems();
+ while (it.hasNext()) {
+ item = (Item) it.next();
+ info = manager.discoverInfo(item.getEntityID());
+ Iterator itx = info.getIdentities();
+ while (itx.hasNext()) {
+ identity = (Identity) itx.next();
+ if (identity.getCategory().equalsIgnoreCase("proxy")
+ && identity.getType().equalsIgnoreCase(
+ "bytestreams")) {
+ proxies.add(info.getFrom());
+ }
+ }
+ }
+ }
+ catch (XMPPException e) {
+ return;
+ }
+ if (proxies.size() > 0) {
+ initStreamHosts();
+ }
+
+ }
+
+ private void initStreamHosts() {
+ List streamHosts = new ArrayList();
+ Iterator it = proxies.iterator();
+ IQ query;
+ PacketCollector collector;
+ Bytestream response;
+ while (it.hasNext()) {
+ String jid = it.next().toString();
+ query = new IQ() {
+ public String getChildElementXML() {
+ return "<query xmlns=\"http://jabber.org/protocol/bytestreams\"/>";
+ }
+ };
+ query.setType(IQ.Type.GET);
+ query.setTo(jid);
+
+ collector = connection.createPacketCollector(new PacketIDFilter(
+ query.getPacketID()));
+ connection.sendPacket(query);
+
+ response = (Bytestream) collector.nextResult(SmackConfiguration
+ .getPacketReplyTimeout());
+ if (response != null) {
+ streamHosts.addAll(response.getStreamHosts());
+ }
+ collector.cancel();
+ }
+ this.streamHosts = streamHosts;
+ }
+
+ /**
+ * Returns the packet to send notification to the stream host to activate
+ * the stream.
+ *
+ * @param sessionID The session ID of the file transfer to activate.
+ * @param from
+ * @param to The JID of the stream host
+ * @param target The JID of the file transfer target.
+ * @return Returns the packet to send notification to the stream host to
+ * activate the stream.
+ */
+ private static Bytestream createByteStreamActivate(final String sessionID,
+ final String from, final String to, final String target) {
+ Bytestream activate = new Bytestream(sessionID);
+ activate.setMode(null);
+ activate.setToActivate(target);
+ activate.setFrom(from);
+ activate.setTo(to);
+ activate.setType(IQ.Type.SET);
+ return activate;
+ }
+
+ /**
+ * Negotiates the Socks 5 bytestream when the local computer is acting as
+ * the proxy.
+ *
+ * @param connection The socket connection with the peer.
+ * @return The SHA-1 digest that is used to uniquely identify the file
+ * transfer.
+ * @throws XMPPException
+ * @throws IOException
+ */
+ private String establishSocks5UploadConnection(Socket connection) throws XMPPException, IOException {
+ OutputStream out = new DataOutputStream(connection.getOutputStream());
+ InputStream in = new DataInputStream(connection.getInputStream());
+
+ // first byte is version should be 5
+ int b = in.read();
+ if (b != 5) {
+ throw new XMPPException("Only SOCKS5 supported");
+ }
+
+ // second byte number of authentication methods supported
+ b = in.read();
+ int[] auth = new int[b];
+ for (int i = 0; i < b; i++) {
+ auth[i] = in.read();
+ }
+
+ int authMethod = -1;
+ for (int i = 0; i < auth.length; i++) {
+ authMethod = (auth[i] == 0 ? 0 : -1); // only auth method
+ // 0, no
+ // authentication,
+ // supported
+ if (authMethod == 0) {
+ break;
+ }
+ }
+ if (authMethod != 0) {
+ throw new XMPPException("Authentication method not supported");
+ }
+ byte[] cmd = new byte[2];
+ cmd[0] = (byte) 0x05;
+ cmd[1] = (byte) 0x00;
+ out.write(cmd);
+
+ String responseDigest = createIncomingSocks5Message(in);
+ cmd = createOutgoingSocks5Message(0, responseDigest);
+
+ if (!connection.isConnected()) {
+ throw new XMPPException("Socket closed by remote user");
+ }
+ out.write(cmd);
+ return responseDigest;
+ }
+
+ public String[] getNamespaces() {
+ return new String[]{NAMESPACE};
+ }
+
+ private void establishSOCKS5ConnectionToProxy(Socket socket, String digest)
+ throws IOException {
+
+ byte[] cmd = new byte[3];
+
+ cmd[0] = (byte) 0x05;
+ cmd[1] = (byte) 0x01;
+ cmd[2] = (byte) 0x00;
+
+ OutputStream out = new DataOutputStream(socket.getOutputStream());
+ out.write(cmd);
+
+ InputStream in = new DataInputStream(socket.getInputStream());
+ byte[] response = new byte[2];
+
+ in.read(response);
+
+ cmd = createOutgoingSocks5Message(1, digest);
+ out.write(cmd);
+ createIncomingSocks5Message(in);
+ }
+
+ private String createIncomingSocks5Message(InputStream in)
+ throws IOException {
+ byte[] cmd = new byte[5];
+ in.read(cmd, 0, 5);
+
+ byte[] addr = new byte[cmd[4]];
+ in.read(addr, 0, addr.length);
+ String digest = new String(addr);
+ in.read();
+ in.read();
+
+ return digest;
+ }
+
+ private byte[] createOutgoingSocks5Message(int cmd, String digest) {
+ byte addr[] = digest.getBytes();
+
+ byte[] data = new byte[7 + addr.length];
+ data[0] = (byte) 5;
+ data[1] = (byte) cmd;
+ data[2] = (byte) 0;
+ data[3] = (byte) 0x3;
+ data[4] = (byte) addr.length;
+
+ System.arraycopy(addr, 0, data, 5, addr.length);
+ data[data.length - 2] = (byte) 0;
+ data[data.length - 1] = (byte) 0;
+
+ return data;
+ }
+
+ public void cleanup() {
+ }
+
+ private static class SelectedHostInfo {
+
+ protected XMPPException exception;
+
+ protected StreamHost selectedHost;
+
+ protected Socket establishedSocket;
+
+ SelectedHostInfo(StreamHost selectedHost, Socket establishedSocket) {
+ this.selectedHost = selectedHost;
+ this.establishedSocket = establishedSocket;
+ }
+
+ public SelectedHostInfo() {
+ }
+ }
+
+ private class ProxyProcess implements Runnable {
+
+ private ServerSocket listeningSocket;
+
+ private Map connectionMap = new HashMap();
+
+ private boolean done = false;
+
+ private Thread thread;
+ private int transfers;
+
+ public void run() {
+ try {
+ listeningSocket.setSoTimeout(10000);
+ }
+ catch (SocketException e) {
+ e.printStackTrace();
+ }
+ while (!done) {
+ Socket conn = null;
+ synchronized (ProxyProcess.this) {
+ while (transfers <= 0) {
+ transfers = -1;
+ try {
+ ProxyProcess.this.wait();
+ }
+ catch (InterruptedException e) {
+ }
+ }
+ }
+ try {
+ synchronized (listeningSocket) {
+ conn = listeningSocket.accept();
+ }
+ if (conn == null) {
+ continue;
+ }
+ String digest = establishSocks5UploadConnection(conn);
+ synchronized (connectionMap) {
+ connectionMap.put(digest, conn);
+ }
+ }
+ catch (IOException e) {
+ }
+ catch (XMPPException e) {
+ e.printStackTrace();
+ if (conn != null) {
+ try {
+ conn.close();
+ }
+ catch (IOException e1) {
+ }
+ }
+ }
+ }
+ }
+
+
+ public void start() {
+ thread.start();
+ }
+
+ public void stop() {
+ done = true;
+ synchronized (this) {
+ this.notify();
+ }
+ }
+
+ public int getPort() {
+ return listeningSocket.getLocalPort();
+ }
+
+ ProxyProcess(ServerSocket listeningSocket) {
+ thread = new Thread(this, "File Transfer Connection Listener");
+ this.listeningSocket = listeningSocket;
+ }
+
+ public Socket getSocket(String digest) {
+ synchronized (connectionMap) {
+ return (Socket) connectionMap.get(digest);
+ }
+ }
+
+ public void addTransfer() {
+ synchronized (this) {
+ if (transfers == -1) {
+ transfers = 1;
+ this.notify();
+ }
+ else {
+ transfers++;
+ }
+ }
+ }
+
+ public void removeTransfer() {
+ synchronized (this) {
+ transfers--;
+ }
+ }
+ }
+
+ private static class BytestreamSIDFilter implements PacketFilter {
+
+ private String sessionID;
+
+ public BytestreamSIDFilter(String sessionID) {
+ if (sessionID == null) {
+ throw new IllegalArgumentException("StreamID cannot be null");
+ }
+ this.sessionID = sessionID;
+ }
+
+ public boolean accept(Packet packet) {
+ if (!Bytestream.class.isInstance(packet)) {
+ return false;
+ }
+ Bytestream bytestream = (Bytestream) packet;
+ String sessionID = bytestream.getSessionID();
+
+ return (sessionID != null && sessionID.equals(this.sessionID));
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java
new file mode 100644
index 000000000..eca2927e2
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java
@@ -0,0 +1,164 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.filetransfer;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * After the file transfer negotiation process is completed according to
+ * JEP-0096, the negotation process is passed off to a particular stream
+ * negotiator. The stream negotiator will then negotiate the chosen stream and
+ * return the stream to transfer the file.
+ *
+ * @author Alexander Wenckus
+ */
+public abstract class StreamNegotiator {
+
+ /**
+ * Creates the initiation acceptance packet to forward to the stream
+ * initiator.
+ *
+ * @param streamInitiationOffer The offer from the stream initatior to connect for a stream.
+ * @param namespaces The namespace that relates to the accepted means of transfer.
+ * @return The response to be forwarded to the initator.
+ */
+ public StreamInitiation createInitiationAccept(
+ StreamInitiation streamInitiationOffer, String [] namespaces) {
+ StreamInitiation response = new StreamInitiation();
+ response.setTo(streamInitiationOffer.getFrom());
+ response.setFrom(streamInitiationOffer.getTo());
+ response.setType(IQ.Type.RESULT);
+ response.setPacketID(streamInitiationOffer.getPacketID());
+
+ DataForm form = new DataForm(Form.TYPE_SUBMIT);
+ FormField field = new FormField(
+ FileTransferNegotiator.STREAM_DATA_FIELD_NAME);
+ for (int i = 0; i < namespaces.length; i++) {
+ field.addValue(namespaces[i]);
+ }
+ form.addField(field);
+
+ response.setFeatureNegotiationForm(form);
+ return response;
+ }
+
+
+ public IQ createError(String from, String to, String packetID, XMPPError xmppError) {
+ IQ iq = FileTransferNegotiator.createIQ(packetID, to, from, IQ.Type.ERROR);
+ iq.setError(xmppError);
+ return iq;
+ }
+
+ Packet initiateIncomingStream(XMPPConnection connection, StreamInitiation initiation) throws XMPPException {
+ StreamInitiation response = createInitiationAccept(initiation,
+ getNamespaces());
+
+ // establish collector to await response
+ PacketCollector collector = connection
+ .createPacketCollector(getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID()));
+ connection.sendPacket(response);
+
+ Packet streamMethodInitiation = collector
+ .nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (streamMethodInitiation == null) {
+ throw new XMPPException("No response from file transfer initiator");
+ }
+
+ return streamMethodInitiation;
+ }
+
+ /**
+ * Returns the packet filter that will return the initiation packet for the appropriate stream
+ * initiation.
+ *
+ * @param from The initiatior of the file transfer.
+ * @param streamID The stream ID related to the transfer.
+ * @return The <b><i>PacketFilter</b></i> that will return the packet relatable to the stream
+ * initiation.
+ */
+ public abstract PacketFilter getInitiationPacketFilter(String from, String streamID);
+
+
+ abstract InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException;
+
+ /**
+ * This method handles the file stream download negotiation process. The
+ * appropriate stream negotiator's initiate incoming stream is called after
+ * an appropriate file transfer method is selected. The manager will respond
+ * to the initatior with the selected means of transfer, then it will handle
+ * any negotation specific to the particular transfer method. This method
+ * returns the InputStream, ready to transfer the file.
+ *
+ * @param initiation The initation that triggered this download.
+ * @return After the negotation process is complete, the InputStream to
+ * write a file to is returned.
+ * @throws XMPPException If an error occurs during this process an XMPPException is
+ * thrown.
+ */
+ public abstract InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException;
+
+ /**
+ * This method handles the file upload stream negotiation process. The
+ * particular stream negotiator is determined during the file transfer
+ * negotiation process. This method returns the OutputStream to transmit the
+ * file to the remote user.
+ *
+ * @param streamID The streamID that uniquely identifies the file transfer.
+ * @param initiator The fully-qualified JID of the initiator of the file transfer.
+ * @param target The fully-qualified JID of the target or reciever of the file
+ * transfer.
+ * @return The negotiated stream ready for data.
+ * @throws XMPPException If an error occurs during the negotiation process an
+ * exception will be thrown.
+ */
+ public abstract OutputStream createOutgoingStream(String streamID,
+ String initiator, String target) throws XMPPException;
+
+ /**
+ * Returns the XMPP namespace reserved for this particular type of file
+ * transfer.
+ *
+ * @return Returns the XMPP namespace reserved for this particular type of
+ * file transfer.
+ */
+ public abstract String[] getNamespaces();
+
+ /**
+ * Cleanup any and all resources associated with this negotiator.
+ */
+ public abstract void cleanup();
+
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Affiliate.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Affiliate.java
new file mode 100644
index 000000000..9300dc6f7
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Affiliate.java
@@ -0,0 +1,98 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+import org.jivesoftware.smackx.packet.MUCAdmin;
+import org.jivesoftware.smackx.packet.MUCOwner;
+
+/**
+ * Represents an affiliation of a user to a given room. The affiliate's information will always have
+ * the bare jid of the real user and its affiliation. If the affiliate is an occupant of the room
+ * then we will also have information about the role and nickname of the user in the room.
+ *
+ * @author Gaston Dombiak
+ */
+public class Affiliate {
+ // Fields that must have a value
+ private String jid;
+ private String affiliation;
+
+ // Fields that may have a value
+ private String role;
+ private String nick;
+
+ Affiliate(MUCOwner.Item item) {
+ super();
+ this.jid = item.getJid();
+ this.affiliation = item.getAffiliation();
+ this.role = item.getRole();
+ this.nick = item.getNick();
+ }
+
+ Affiliate(MUCAdmin.Item item) {
+ super();
+ this.jid = item.getJid();
+ this.affiliation = item.getAffiliation();
+ this.role = item.getRole();
+ this.nick = item.getNick();
+ }
+
+ /**
+ * Returns the bare JID of the affiliated user. This information will always be available.
+ *
+ * @return the bare JID of the affiliated user.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the affiliation of the afffiliated user. Possible affiliations are: "owner", "admin",
+ * "member", "outcast". This information will always be available.
+ *
+ * @return the affiliation of the afffiliated user.
+ */
+ public String getAffiliation() {
+ return affiliation;
+ }
+
+ /**
+ * Returns the current role of the affiliated user if the user is currently in the room.
+ * If the user is not present in the room then the answer will be null.
+ *
+ * @return the current role of the affiliated user in the room or null if the user is not in
+ * the room.
+ */
+ public String getRole() {
+ return role;
+ }
+
+ /**
+ * Returns the current nickname of the affiliated user if the user is currently in the room.
+ * If the user is not present in the room then the answer will be null.
+ *
+ * @return the current nickname of the affiliated user in the room or null if the user is not in
+ * the room.
+ */
+ public String getNick() {
+ return nick;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java
new file mode 100644
index 000000000..6d87bb56d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java
@@ -0,0 +1,76 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+import org.jivesoftware.smack.PacketInterceptor;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ * Packet interceptor that will intercept presence packets sent to the MUC service to indicate
+ * that the user wants to be a deaf occupant. A user can only indicate that he wants to be a
+ * deaf occupant while joining the room. It is not possible to become deaf or stop being deaf
+ * after the user joined the room.<p>
+ *
+ * Deaf occupants will not get messages broadcasted to all room occupants. However, they will
+ * be able to get private messages, presences, IQ packets or room history. To use this
+ * functionality you will need to send the message
+ * {@link MultiUserChat#addPresenceInterceptor(org.jivesoftware.smack.PacketInterceptor)} and
+ * pass this interceptor as the parameter.<p>
+ *
+ * Note that this is a custom extension to the MUC service so it may not work with other servers
+ * than Wildfire.
+ *
+ * @author Gaston Dombiak
+ */
+public class DeafOccupantInterceptor implements PacketInterceptor {
+
+ public void interceptPacket(Packet packet) {
+ Presence presence = (Presence) packet;
+ // Check if user is joining a room
+ if (Presence.Type.AVAILABLE == presence.getType() &&
+ presence.getExtension("x", "http://jabber.org/protocol/muc") != null) {
+ // Add extension that indicates that user wants to be a deaf occupant
+ packet.addExtension(new DeafExtension());
+ }
+ }
+
+ private static class DeafExtension implements PacketExtension {
+
+ public String getElementName() {
+ return "x";
+ }
+
+ public String getNamespace() {
+ return "http://jivesoftware.org/protocol/muc";
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace())
+ .append("\">");
+ buf.append("<deaf-occupant/>");
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java
new file mode 100644
index 000000000..5974710e6
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java
@@ -0,0 +1,79 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+/**
+ * Default implementation of the ParticipantStatusListener interface.<p>
+ *
+ * This class does not provide any behavior by default. It just avoids having
+ * to implement all the inteface methods if the user is only interested in implementing
+ * some of the methods.
+ *
+ * @author Gaston Dombiak
+ */
+public class DefaultParticipantStatusListener implements ParticipantStatusListener {
+
+ public void joined(String participant) {
+ }
+
+ public void left(String participant) {
+ }
+
+ public void kicked(String participant, String actor, String reason) {
+ }
+
+ public void voiceGranted(String participant) {
+ }
+
+ public void voiceRevoked(String participant) {
+ }
+
+ public void banned(String participant, String actor, String reason) {
+ }
+
+ public void membershipGranted(String participant) {
+ }
+
+ public void membershipRevoked(String participant) {
+ }
+
+ public void moderatorGranted(String participant) {
+ }
+
+ public void moderatorRevoked(String participant) {
+ }
+
+ public void ownershipGranted(String participant) {
+ }
+
+ public void ownershipRevoked(String participant) {
+ }
+
+ public void adminGranted(String participant) {
+ }
+
+ public void adminRevoked(String participant) {
+ }
+
+ public void nicknameChanged(String participant, String newNickname) {
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java
new file mode 100644
index 000000000..1075d6ca0
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java
@@ -0,0 +1,70 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+/**
+ * Default implementation of the UserStatusListener interface.<p>
+ *
+ * This class does not provide any behavior by default. It just avoids having
+ * to implement all the inteface methods if the user is only interested in implementing
+ * some of the methods.
+ *
+ * @author Gaston Dombiak
+ */
+public class DefaultUserStatusListener implements UserStatusListener {
+
+ public void kicked(String actor, String reason) {
+ }
+
+ public void voiceGranted() {
+ }
+
+ public void voiceRevoked() {
+ }
+
+ public void banned(String actor, String reason) {
+ }
+
+ public void membershipGranted() {
+ }
+
+ public void membershipRevoked() {
+ }
+
+ public void moderatorGranted() {
+ }
+
+ public void moderatorRevoked() {
+ }
+
+ public void ownershipGranted() {
+ }
+
+ public void ownershipRevoked() {
+ }
+
+ public void adminGranted() {
+ }
+
+ public void adminRevoked() {
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DiscussionHistory.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DiscussionHistory.java
new file mode 100644
index 000000000..a5edc6fc8
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DiscussionHistory.java
@@ -0,0 +1,173 @@
+/**
+ * $RCSfile$
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+import java.util.Date;
+
+import org.jivesoftware.smackx.packet.MUCInitialPresence;
+
+/**
+ * The DiscussionHistory class controls the number of characters or messages to receive
+ * when entering a room. The room will decide the amount of history to return if you don't
+ * specify a DiscussionHistory while joining a room.<p>
+ *
+ * You can use some or all of these variable to control the amount of history to receive:
+ * <ul>
+ * <li>maxchars -> total number of characters to receive in the history.
+ * <li>maxstanzas -> total number of messages to receive in the history.
+ * <li>seconds -> only the messages received in the last "X" seconds will be included in the
+ * history.
+ * <li>since -> only the messages received since the datetime specified will be included in
+ * the history.
+ * </ul>
+ *
+ * Note: Setting maxchars to 0 indicates that the user requests to receive no history.
+ *
+ * @author Gaston Dombiak
+ */
+public class DiscussionHistory {
+
+ private int maxChars = -1;
+ private int maxStanzas = -1;
+ private int seconds = -1;
+ private Date since;
+
+ /**
+ * Returns the total number of characters to receive in the history.
+ *
+ * @return total number of characters to receive in the history.
+ */
+ public int getMaxChars() {
+ return maxChars;
+ }
+
+ /**
+ * Returns the total number of messages to receive in the history.
+ *
+ * @return the total number of messages to receive in the history.
+ */
+ public int getMaxStanzas() {
+ return maxStanzas;
+ }
+
+ /**
+ * Returns the number of seconds to use to filter the messages received during that time.
+ * In other words, only the messages received in the last "X" seconds will be included in
+ * the history.
+ *
+ * @return the number of seconds to use to filter the messages received during that time.
+ */
+ public int getSeconds() {
+ return seconds;
+ }
+
+ /**
+ * Returns the since date to use to filter the messages received during that time.
+ * In other words, only the messages received since the datetime specified will be
+ * included in the history.
+ *
+ * @return the since date to use to filter the messages received during that time.
+ */
+ public Date getSince() {
+ return since;
+ }
+
+ /**
+ * Sets the total number of characters to receive in the history.
+ *
+ * @param maxChars the total number of characters to receive in the history.
+ */
+ public void setMaxChars(int maxChars) {
+ this.maxChars = maxChars;
+ }
+
+ /**
+ * Sets the total number of messages to receive in the history.
+ *
+ * @param maxStanzas the total number of messages to receive in the history.
+ */
+ public void setMaxStanzas(int maxStanzas) {
+ this.maxStanzas = maxStanzas;
+ }
+
+ /**
+ * Sets the number of seconds to use to filter the messages received during that time.
+ * In other words, only the messages received in the last "X" seconds will be included in
+ * the history.
+ *
+ * @param seconds the number of seconds to use to filter the messages received during
+ * that time.
+ */
+ public void setSeconds(int seconds) {
+ this.seconds = seconds;
+ }
+
+ /**
+ * Sets the since date to use to filter the messages received during that time.
+ * In other words, only the messages received since the datetime specified will be
+ * included in the history.
+ *
+ * @param since the since date to use to filter the messages received during that time.
+ */
+ public void setSince(Date since) {
+ this.since = since;
+ }
+
+ /**
+ * Returns true if the history has been configured with some values.
+ *
+ * @return true if the history has been configured with some values.
+ */
+ private boolean isConfigured() {
+ return maxChars > -1 || maxStanzas > -1 || seconds > -1 || since != null;
+ }
+
+ /**
+ * Returns the History that manages the amount of discussion history provided on entering a
+ * room.
+ *
+ * @return the History that manages the amount of discussion history provided on entering a
+ * room.
+ */
+ MUCInitialPresence.History getMUCHistory() {
+ // Return null if the history was not properly configured
+ if (!isConfigured()) {
+ return null;
+ }
+
+ MUCInitialPresence.History mucHistory = new MUCInitialPresence.History();
+ if (maxChars > -1) {
+ mucHistory.setMaxChars(maxChars);
+ }
+ if (maxStanzas > -1) {
+ mucHistory.setMaxStanzas(maxStanzas);
+ }
+ if (seconds > -1) {
+ mucHistory.setSeconds(seconds);
+ }
+ if (since != null) {
+ mucHistory.setSince(since);
+ }
+ return mucHistory;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/HostedRoom.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/HostedRoom.java
new file mode 100644
index 000000000..51905a346
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/HostedRoom.java
@@ -0,0 +1,65 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+import org.jivesoftware.smackx.packet.DiscoverItems;
+
+/**
+ * Hosted rooms by a chat service may be discovered if they are configured to appear in the room
+ * directory . The information that may be discovered is the XMPP address of the room and the room
+ * name. The address of the room may be used for obtaining more detailed information
+ * {@link org.jivesoftware.smackx.muc.MultiUserChat#getRoomInfo(org.jivesoftware.smack.XMPPConnection, String)}
+ * or could be used for joining the room
+ * {@link org.jivesoftware.smackx.muc.MultiUserChat#MultiUserChat(org.jivesoftware.smack.XMPPConnection, String)}
+ * and {@link org.jivesoftware.smackx.muc.MultiUserChat#join(String)}.
+ *
+ * @author Gaston Dombiak
+ */
+public class HostedRoom {
+
+ private String jid;
+
+ private String name;
+
+ public HostedRoom(DiscoverItems.Item item) {
+ super();
+ jid = item.getEntityID();
+ name = item.getName();
+ }
+
+ /**
+ * Returns the XMPP address of the hosted room by the chat service. This address may be used
+ * when creating a <code>MultiUserChat</code> when joining a room.
+ *
+ * @return the XMPP address of the hosted room by the chat service.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the name of the room.
+ *
+ * @return the name of the room.
+ */
+ public String getName() {
+ return name;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationListener.java
new file mode 100644
index 000000000..eeb814e7a
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationListener.java
@@ -0,0 +1,49 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.packet.Message;
+
+/**
+ * A listener that is fired anytime an invitation to join a MUC room is received.
+ *
+ * @author Gaston Dombiak
+ */
+public interface InvitationListener {
+
+ /**
+ * Called when the an invitation to join a MUC room is received.<p>
+ *
+ * If the room is password-protected, the invitee will receive a password to use to join
+ * the room. If the room is members-only, the the invitee may be added to the member list.
+ *
+ * @param conn the XMPPConnection that received the invitation.
+ * @param room the room that invitation refers to.
+ * @param inviter the inviter that sent the invitation. (e.g. crone1@shakespeare.lit).
+ * @param reason the reason why the inviter sent the invitation.
+ * @param password the password to use when joining the room.
+ * @param message the message used by the inviter to send the invitation.
+ */
+ public abstract void invitationReceived(XMPPConnection conn, String room, String inviter, String reason,
+ String password, Message message);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java
new file mode 100644
index 000000000..81ae0ab55
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java
@@ -0,0 +1,38 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+/**
+ * A listener that is fired anytime an invitee declines or rejects an invitation.
+ *
+ * @author Gaston Dombiak
+ */
+public interface InvitationRejectionListener {
+
+ /**
+ * Called when the invitee declines the invitation.
+ *
+ * @param invitee the invitee that declined the invitation. (e.g. hecate@shakespeare.lit).
+ * @param reason the reason why the invitee declined the invitation.
+ */
+ public abstract void invitationDeclined(String invitee, String reason);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/MultiUserChat.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/MultiUserChat.java
new file mode 100644
index 000000000..080084285
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/MultiUserChat.java
@@ -0,0 +1,2668 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.NodeInformationProvider;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.*;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+
+/**
+ * A MultiUserChat is a conversation that takes place among many users in a virtual
+ * room. A room could have many occupants with different affiliation and roles.
+ * Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles
+ * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
+ * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
+ * Grant voice, Edit member list, etc.).
+ *
+ * @author Gaston Dombiak
+ */
+public class MultiUserChat {
+
+ private final static String discoNamespace = "http://jabber.org/protocol/muc";
+ private final static String discoNode = "http://jabber.org/protocol/muc#rooms";
+
+ private static Map joinedRooms = new WeakHashMap();
+
+ private XMPPConnection connection;
+ private String room;
+ private String subject;
+ private String nickname = null;
+ private boolean joined = false;
+ private Map occupantsMap = new HashMap();
+
+ private List invitationRejectionListeners = new ArrayList();
+ private List subjectUpdatedListeners = new ArrayList();
+ private List userStatusListeners = new ArrayList();
+ private List participantStatusListeners = new ArrayList();
+
+ private PacketFilter presenceFilter;
+ private PacketListener presenceListener;
+ private List presenceInterceptors = new ArrayList();
+ private PacketFilter subjectFilter;
+ private PacketListener subjectListener;
+ private PacketFilter messageFilter;
+ private PacketFilter declinesFilter;
+ private PacketListener declinesListener;
+ private PacketCollector messageCollector;
+ private List connectionListeners = new ArrayList();
+
+ static {
+ XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() {
+ public void connectionEstablished(final XMPPConnection connection) {
+ // Set on every established connection that this client supports the Multi-User
+ // Chat protocol. This information will be used when another client tries to
+ // discover whether this client supports MUC or not.
+ ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace);
+ // Set the NodeInformationProvider that will provide information about the
+ // joined rooms whenever a disco request is received
+ ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(
+ discoNode,
+ new NodeInformationProvider() {
+ public Iterator getNodeItems() {
+ ArrayList answer = new ArrayList();
+ Iterator rooms=MultiUserChat.getJoinedRooms(connection);
+ while (rooms.hasNext()) {
+ answer.add(new DiscoverItems.Item((String)rooms.next()));
+ }
+ return answer.iterator();
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Creates a new multi user chat with the specified connection and room name. Note: no
+ * information is sent to or received from the server until you attempt to
+ * {@link #join(String) join} the chat room. On some server implementations,
+ * the room will not be created until the first person joins it.<p>
+ *
+ * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
+ * for the XMPP server example.com). You must ensure that the room address you're
+ * trying to connect to includes the proper chat sub-domain.
+ *
+ * @param connection the XMPP connection.
+ * @param room the name of the room in the form "roomName@service", where
+ * "service" is the hostname at which the multi-user chat
+ * service is running. Make sure to provide a valid JID.
+ */
+ public MultiUserChat(XMPPConnection connection, String room) {
+ this.connection = connection;
+ this.room = room.toLowerCase();
+ init();
+ }
+
+ /**
+ * Returns true if the specified user supports the Multi-User Chat protocol.
+ *
+ * @param connection the connection to use to perform the service discovery.
+ * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
+ * @return a boolean indicating whether the specified user supports the MUC protocol.
+ */
+ public static boolean isServiceEnabled(XMPPConnection connection, String user) {
+ try {
+ DiscoverInfo result =
+ ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user);
+ return result.containsFeature(discoNamespace);
+ }
+ catch (XMPPException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * Returns an Iterator on the rooms where the user has joined using a given connection.
+ * The Iterator will contain Strings where each String represents a room
+ * (e.g. room@muc.jabber.org).
+ *
+ * @param connection the connection used to join the rooms.
+ * @return an Iterator on the rooms where the user has joined using a given connection.
+ */
+ private static Iterator getJoinedRooms(XMPPConnection connection) {
+ ArrayList rooms = (ArrayList)joinedRooms.get(connection);
+ if (rooms != null) {
+ return rooms.iterator();
+ }
+ // Return an iterator on an empty collection (i.e. the user never joined a room)
+ return new ArrayList().iterator();
+ }
+
+ /**
+ * Returns an Iterator on the rooms where the requested user has joined. The Iterator will
+ * contain Strings where each String represents a room (e.g. room@muc.jabber.org).
+ *
+ * @param connection the connection to use to perform the service discovery.
+ * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
+ * @return an Iterator on the rooms where the requested user has joined.
+ */
+ public static Iterator getJoinedRooms(XMPPConnection connection, String user) {
+ try {
+ ArrayList answer = new ArrayList();
+ // Send the disco packet to the user
+ DiscoverItems result =
+ ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode);
+ // Collect the entityID for each returned item
+ for (Iterator items=result.getItems(); items.hasNext();) {
+ answer.add(((DiscoverItems.Item)items.next()).getEntityID());
+ }
+ return answer.iterator();
+ }
+ catch (XMPPException e) {
+ e.printStackTrace();
+ // Return an iterator on an empty collection
+ return new ArrayList().iterator();
+ }
+ }
+
+ /**
+ * Returns the discovered information of a given room without actually having to join the room.
+ * The server will provide information only for rooms that are public.
+ *
+ * @param connection the XMPP connection to use for discovering information about the room.
+ * @param room the name of the room in the form "roomName@service" of which we want to discover
+ * its information.
+ * @return the discovered information of a given room without actually having to join the room.
+ * @throws XMPPException if an error occured while trying to discover information of a room.
+ */
+ public static RoomInfo getRoomInfo(XMPPConnection connection, String room)
+ throws XMPPException {
+ DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room);
+ return new RoomInfo(info);
+ }
+
+ /**
+ * Returns a collection with the XMPP addresses of the Multi-User Chat services.
+ *
+ * @param connection the XMPP connection to use for discovering Multi-User Chat services.
+ * @return a collection with the XMPP addresses of the Multi-User Chat services.
+ * @throws XMPPException if an error occured while trying to discover MUC services.
+ */
+ public static Collection getServiceNames(XMPPConnection connection) throws XMPPException {
+ final List answer = new ArrayList();
+ ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
+ DiscoverItems items = discoManager.discoverItems(connection.getServiceName());
+ for (Iterator it = items.getItems(); it.hasNext();) {
+ DiscoverItems.Item item = (DiscoverItems.Item) it.next();
+ try {
+ DiscoverInfo info = discoManager.discoverInfo(item.getEntityID());
+ if (info.containsFeature("http://jabber.org/protocol/muc")) {
+ answer.add(item.getEntityID());
+ }
+ }
+ catch (XMPPException e) {
+ // Trouble finding info in some cases. This is a workaround for
+ // discovering info on remote servers.
+ }
+ }
+ return answer;
+ }
+
+ /**
+ * Returns a collection 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 connection the XMPP connection to use for discovering hosted rooms by the MUC service.
+ * @param serviceName the service that is hosting the rooms to discover.
+ * @return a collection of HostedRooms.
+ * @throws XMPPException if an error occured while trying to discover the information.
+ */
+ public static Collection getHostedRooms(XMPPConnection connection, String serviceName)
+ throws XMPPException {
+ List answer = new ArrayList();
+ ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
+ DiscoverItems items = discoManager.discoverItems(serviceName);
+ for (Iterator it = items.getItems(); it.hasNext();) {
+ DiscoverItems.Item item = (DiscoverItems.Item) it.next();
+ answer.add(new HostedRoom(item));
+ }
+ return answer;
+ }
+
+ /**
+ * Returns the name of the room this MultiUserChat object represents.
+ *
+ * @return the multi user chat room name.
+ */
+ public String getRoom() {
+ return room;
+ }
+
+ /**
+ * Creates the room according to some default configuration, assign the requesting user
+ * as the room owner, and add the owner to the room but not allow anyone else to enter
+ * the room (effectively "locking" the room). The requesting user will join the room
+ * under the specified nickname as soon as the room has been created.<p>
+ *
+ * To create an "Instant Room", that means a room with some default configuration that is
+ * available for immediate access, the room's owner should send an empty form after creating
+ * the room. {@link #sendConfigurationForm(Form)}<p>
+ *
+ * To create a "Reserved Room", that means a room manually configured by the room creator
+ * before anyone is allowed to enter, the room's owner should complete and send a form after
+ * creating the room. Once the completed configutation form is sent to the server, the server
+ * will unlock the room. {@link #sendConfigurationForm(Form)}
+ *
+ * @param nickname the nickname to use.
+ * @throws XMPPException if the room couldn't be created for some reason
+ * (e.g. room already exists; user already joined to an existant room or
+ * 405 error if the user is not allowed to create the room)
+ */
+ public synchronized void create(String nickname) throws XMPPException {
+ if (nickname == null || nickname.equals("")) {
+ throw new IllegalArgumentException("Nickname must not be null or blank.");
+ }
+ // If we've already joined the room, leave it before joining under a new
+ // nickname.
+ if (joined) {
+ throw new IllegalStateException("Creation failed - User already joined the room.");
+ }
+ // We create a room by sending a presence packet to room@service/nick
+ // and signal support for MUC. The owner will be automatically logged into the room.
+ Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
+ joinPresence.setTo(room + "/" + nickname);
+ // Indicate the the client supports MUC
+ joinPresence.addExtension(new MUCInitialPresence());
+ // Invoke presence interceptors so that extra information can be dynamically added
+ for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) {
+ PacketInterceptor packetInterceptor = (PacketInterceptor) it.next();
+ packetInterceptor.interceptPacket(joinPresence);
+ }
+
+ // Wait for a presence packet back from the server.
+ PacketFilter responseFilter =
+ new AndFilter(
+ new FromMatchesFilter(room + "/" + nickname),
+ new PacketTypeFilter(Presence.class));
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send create & join packet.
+ connection.sendPacket(joinPresence);
+ // Wait up to a certain number of seconds for a reply.
+ Presence presence =
+ (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (presence == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+ // Whether the room existed before or was created, the user has joined the room
+ this.nickname = nickname;
+ joined = true;
+ userHasJoined();
+
+ // Look for confirmation of room creation from the server
+ MUCUser mucUser = getMUCUserExtension(presence);
+ if (mucUser != null && mucUser.getStatus() != null) {
+ if ("201".equals(mucUser.getStatus().getCode())) {
+ // Room was created and the user has joined the room
+ return;
+ }
+ }
+ // We need to leave the room since it seems that the room already existed
+ leave();
+ throw new XMPPException("Creation failed - Missing acknowledge of room creation.");
+ }
+
+ /**
+ * Joins the chat room using the specified nickname. If already joined
+ * using another nickname, this method will first leave the room and then
+ * re-join using the new nickname. The default timeout of Smack for a reply
+ * from the group chat server that the join succeeded will be used. After
+ * joining the room, the room will decide the amount of history to send.
+ *
+ * @param nickname the nickname to use.
+ * @throws XMPPException if an error occurs joining the room. In particular, a
+ * 401 error can occur if no password was provided and one is required; or a
+ * 403 error can occur if the user is banned; or a
+ * 404 error can occur if the room does not exist or is locked; or a
+ * 407 error can occur if user is not on the member list; or a
+ * 409 error can occur if someone is already in the group chat with the same nickname.
+ */
+ public void join(String nickname) throws XMPPException {
+ join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout());
+ }
+
+ /**
+ * Joins the chat room using the specified nickname and password. If already joined
+ * using another nickname, this method will first leave the room and then
+ * re-join using the new nickname. The default timeout of Smack for a reply
+ * from the group chat server that the join succeeded will be used. After
+ * joining the room, the room will decide the amount of history to send.<p>
+ *
+ * A password is required when joining password protected rooms. If the room does
+ * not require a password there is no need to provide one.
+ *
+ * @param nickname the nickname to use.
+ * @param password the password to use.
+ * @throws XMPPException if an error occurs joining the room. In particular, a
+ * 401 error can occur if no password was provided and one is required; or a
+ * 403 error can occur if the user is banned; or a
+ * 404 error can occur if the room does not exist or is locked; or a
+ * 407 error can occur if user is not on the member list; or a
+ * 409 error can occur if someone is already in the group chat with the same nickname.
+ */
+ public void join(String nickname, String password) throws XMPPException {
+ join(nickname, password, null, SmackConfiguration.getPacketReplyTimeout());
+ }
+
+ /**
+ * Joins the chat room using the specified nickname and password. If already joined
+ * using another nickname, this method will first leave the room and then
+ * re-join using the new nickname.<p>
+ *
+ * To control the amount of history to receive while joining a room you will need to provide
+ * a configured DiscussionHistory object.<p>
+ *
+ * A password is required when joining password protected rooms. If the room does
+ * not require a password there is no need to provide one.<p>
+ *
+ * If the room does not already exist when the user seeks to enter it, the server will
+ * decide to create a new room or not.
+ *
+ * @param nickname the nickname to use.
+ * @param password the password to use.
+ * @param history the amount of discussion history to receive while joining a room.
+ * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds).
+ * @throws XMPPException if an error occurs joining the room. In particular, a
+ * 401 error can occur if no password was provided and one is required; or a
+ * 403 error can occur if the user is banned; or a
+ * 404 error can occur if the room does not exist or is locked; or a
+ * 407 error can occur if user is not on the member list; or a
+ * 409 error can occur if someone is already in the group chat with the same nickname.
+ */
+ public synchronized void join(
+ String nickname,
+ String password,
+ DiscussionHistory history,
+ long timeout)
+ throws XMPPException {
+ if (nickname == null || nickname.equals("")) {
+ throw new IllegalArgumentException("Nickname must not be null or blank.");
+ }
+ // If we've already joined the room, leave it before joining under a new
+ // nickname.
+ if (joined) {
+ leave();
+ }
+ // We join a room by sending a presence packet where the "to"
+ // field is in the form "roomName@service/nickname"
+ Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
+ joinPresence.setTo(room + "/" + nickname);
+
+ // Indicate the the client supports MUC
+ MUCInitialPresence mucInitialPresence = new MUCInitialPresence();
+ if (password != null) {
+ mucInitialPresence.setPassword(password);
+ }
+ if (history != null) {
+ mucInitialPresence.setHistory(history.getMUCHistory());
+ }
+ joinPresence.addExtension(mucInitialPresence);
+ // Invoke presence interceptors so that extra information can be dynamically added
+ for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) {
+ PacketInterceptor packetInterceptor = (PacketInterceptor) it.next();
+ packetInterceptor.interceptPacket(joinPresence);
+ }
+
+ // Wait for a presence packet back from the server.
+ PacketFilter responseFilter =
+ new AndFilter(
+ new FromMatchesFilter(room + "/" + nickname),
+ new PacketTypeFilter(Presence.class));
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send join packet.
+ connection.sendPacket(joinPresence);
+ // Wait up to a certain number of seconds for a reply.
+ Presence presence = (Presence) response.nextResult(timeout);
+ // Stop queuing results
+ response.cancel();
+
+ if (presence == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+ this.nickname = nickname;
+ joined = true;
+ userHasJoined();
+ }
+
+ /**
+ * Returns true if currently in the multi user chat (after calling the {@link
+ * #join(String)} method).
+ *
+ * @return true if currently in the multi user chat room.
+ */
+ public boolean isJoined() {
+ return joined;
+ }
+
+ /**
+ * Leave the chat room.
+ */
+ public synchronized void leave() {
+ // If not joined already, do nothing.
+ if (!joined) {
+ return;
+ }
+ // 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(room + "/" + nickname);
+ // Invoke presence interceptors so that extra information can be dynamically added
+ for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) {
+ PacketInterceptor packetInterceptor = (PacketInterceptor) it.next();
+ packetInterceptor.interceptPacket(leavePresence);
+ }
+ connection.sendPacket(leavePresence);
+ // Reset occupant information.
+ occupantsMap = new HashMap();
+ nickname = null;
+ joined = false;
+ userHasLeft();
+ }
+
+ /**
+ * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if
+ * no configuration is possible. The configuration form allows to set the room's language,
+ * enable logging, specify room's type, etc..
+ *
+ * @return the Form that contains the fields to complete together with the instrucions or
+ * <tt>null</tt> if no configuration is possible.
+ * @throws XMPPException if an error occurs asking the configuration form for the room.
+ */
+ public Form getConfigurationForm() throws XMPPException {
+ MUCOwner iq = new MUCOwner();
+ iq.setTo(room);
+ iq.setType(IQ.Type.GET);
+
+ // Filter packets looking for an answer from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Request the configuration form to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ return Form.getFormFrom(answer);
+ }
+
+ /**
+ * Sends the completed configuration form to the server. The room will be configured
+ * with the new settings defined in the form. If the form is empty then the server
+ * will create an instant room (will use default configuration).
+ *
+ * @param form the form with the new settings.
+ * @throws XMPPException if an error occurs setting the new rooms' configuration.
+ */
+ public void sendConfigurationForm(Form form) throws XMPPException {
+ MUCOwner iq = new MUCOwner();
+ iq.setTo(room);
+ iq.setType(IQ.Type.SET);
+ iq.addExtension(form.getDataFormToSend());
+
+ // Filter packets looking for an answer from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the completed configuration form to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ /**
+ * Returns the room's registration form that an unaffiliated user, can use to become a member
+ * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the
+ * privilege to register members and allow only room admins to add new members.<p>
+ *
+ * If the user requesting registration requirements is not allowed to register with the room
+ * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
+ * error to the user (error code 405).
+ *
+ * @return the registration Form that contains the fields to complete together with the
+ * instrucions or <tt>null</tt> if no registration is possible.
+ * @throws XMPPException if an error occurs asking the registration form for the room or a
+ * 405 error if the user is not allowed to register with the room.
+ */
+ public Form getRegistrationForm() throws XMPPException {
+ Registration reg = new Registration();
+ reg.setType(IQ.Type.GET);
+ reg.setTo(room);
+
+ PacketFilter filter =
+ new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ return Form.getFormFrom(result);
+ }
+
+ /**
+ * Sends the completed registration form to the server. After the user successfully submits
+ * the form, the room may queue the request for review by the room admins or may immediately
+ * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
+ *
+ * If the desired room nickname is already reserved for that room, the room will return a
+ * "Conflict" error to the user (error code 409). If the room does not support registration,
+ * it will return a "Service Unavailable" error to the user (error code 503).
+ *
+ * @param form the completed registration form.
+ * @throws XMPPException if an error occurs submitting the registration form. In particular, a
+ * 409 error can occur if the desired room nickname is already reserved for that room;
+ * or a 503 error can occur if the room does not support registration.
+ */
+ public void sendRegistrationForm(Form form) throws XMPPException {
+ Registration reg = new Registration();
+ reg.setType(IQ.Type.SET);
+ reg.setTo(room);
+ reg.addExtension(form.getDataFormToSend());
+
+ PacketFilter filter =
+ new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ }
+
+ /**
+ * Sends a request to the server to destroy the room. The sender of the request
+ * should be the room's owner. If the sender of the destroy request is not the room's owner
+ * then the server will answer a "Forbidden" error (403).
+ *
+ * @param reason the reason for the room destruction.
+ * @param alternateJID the JID of an alternate location.
+ * @throws XMPPException if an error occurs while trying to destroy the room.
+ * An error can occur which will be wrapped by an XMPPException --
+ * XMPP error code 403. The error code can be used to present more
+ * appropiate error messages to end-users.
+ */
+ public void destroy(String reason, String alternateJID) throws XMPPException {
+ MUCOwner iq = new MUCOwner();
+ iq.setTo(room);
+ iq.setType(IQ.Type.SET);
+
+ // Create the reason for the room destruction
+ MUCOwner.Destroy destroy = new MUCOwner.Destroy();
+ destroy.setReason(reason);
+ destroy.setJid(alternateJID);
+ iq.setDestroy(destroy);
+
+ // Wait for a presence packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the room destruction request.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ // Reset occupant information.
+ occupantsMap = new HashMap();
+ nickname = null;
+ joined = false;
+ userHasLeft();
+ }
+
+ /**
+ * Invites another user to the room in which one is an occupant. The invitation
+ * will be sent to the room which in turn will forward the invitation to the invitee.<p>
+ *
+ * If the room is password-protected, the invitee will receive a password to use to join
+ * the room. If the room is members-only, the the invitee may be added to the member list.
+ *
+ * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
+ * @param reason the reason why the user is being invited.
+ */
+ public void invite(String user, String reason) {
+ invite(new Message(), user, reason);
+ }
+
+ /**
+ * Invites another user to the room in which one is an occupant using a given Message. The invitation
+ * will be sent to the room which in turn will forward the invitation to the invitee.<p>
+ *
+ * If the room is password-protected, the invitee will receive a password to use to join
+ * the room. If the room is members-only, the the invitee may be added to the member list.
+ *
+ * @param message the message to use for sending the invitation.
+ * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
+ * @param reason the reason why the user is being invited.
+ */
+ public void invite(Message message, String user, String reason) {
+ // TODO listen for 404 error code when inviter supplies a non-existent JID
+ message.setTo(room);
+
+ // Create the MUCUser packet that will include the invitation
+ MUCUser mucUser = new MUCUser();
+ MUCUser.Invite invite = new MUCUser.Invite();
+ invite.setTo(user);
+ invite.setReason(reason);
+ mucUser.setInvite(invite);
+ // Add the MUCUser packet that includes the invitation to the message
+ message.addExtension(mucUser);
+
+ connection.sendPacket(message);
+ }
+
+ /**
+ * Informs the sender of an invitation that the invitee declines the invitation. The rejection
+ * will be sent to the room which in turn will forward the rejection to the inviter.
+ *
+ * @param conn the connection to use for sending the rejection.
+ * @param room the room that sent the original invitation.
+ * @param inviter the inviter of the declined invitation.
+ * @param reason the reason why the invitee is declining the invitation.
+ */
+ public static void decline(XMPPConnection conn, String room, String inviter, String reason) {
+ Message message = new Message(room);
+
+ // Create the MUCUser packet that will include the rejection
+ MUCUser mucUser = new MUCUser();
+ MUCUser.Decline decline = new MUCUser.Decline();
+ decline.setTo(inviter);
+ decline.setReason(reason);
+ mucUser.setDecline(decline);
+ // Add the MUCUser packet that includes the rejection
+ message.addExtension(mucUser);
+
+ conn.sendPacket(message);
+ }
+
+ /**
+ * Adds a listener to invitation notifications. The listener will be fired anytime
+ * an invitation is received.
+ *
+ * @param conn the connection where the listener will be applied.
+ * @param listener an invitation listener.
+ */
+ public static void addInvitationListener(XMPPConnection conn, InvitationListener listener) {
+ InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener);
+ }
+
+ /**
+ * Removes a listener to invitation notifications. The listener will be fired anytime
+ * an invitation is received.
+ *
+ * @param conn the connection where the listener was applied.
+ * @param listener an invitation listener.
+ */
+ public static void removeInvitationListener(XMPPConnection conn, InvitationListener listener) {
+ InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener);
+ }
+
+ /**
+ * Adds a listener to invitation rejections notifications. The listener will be fired anytime
+ * an invitation is declined.
+ *
+ * @param listener an invitation rejection listener.
+ */
+ public void addInvitationRejectionListener(InvitationRejectionListener listener) {
+ synchronized (invitationRejectionListeners) {
+ if (!invitationRejectionListeners.contains(listener)) {
+ invitationRejectionListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener from invitation rejections notifications. The listener will be fired
+ * anytime an invitation is declined.
+ *
+ * @param listener an invitation rejection listener.
+ */
+ public void removeInvitationRejectionListener(InvitationRejectionListener listener) {
+ synchronized (invitationRejectionListeners) {
+ invitationRejectionListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Fires invitation rejection listeners.
+ */
+ private void fireInvitationRejectionListeners(String invitee, String reason) {
+ InvitationRejectionListener[] listeners = null;
+ synchronized (invitationRejectionListeners) {
+ listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
+ invitationRejectionListeners.toArray(listeners);
+ }
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].invitationDeclined(invitee, reason);
+ }
+ }
+
+ /**
+ * Adds a listener to subject change notifications. The listener will be fired anytime
+ * the room's subject changes.
+ *
+ * @param listener a subject updated listener.
+ */
+ public void addSubjectUpdatedListener(SubjectUpdatedListener listener) {
+ synchronized (subjectUpdatedListeners) {
+ if (!subjectUpdatedListeners.contains(listener)) {
+ subjectUpdatedListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener from subject change notifications. The listener will be fired
+ * anytime the room's subject changes.
+ *
+ * @param listener a subject updated listener.
+ */
+ public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
+ synchronized (subjectUpdatedListeners) {
+ subjectUpdatedListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Fires subject updated listeners.
+ */
+ private void fireSubjectUpdatedListeners(String subject, String from) {
+ SubjectUpdatedListener[] listeners = null;
+ synchronized (subjectUpdatedListeners) {
+ listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()];
+ subjectUpdatedListeners.toArray(listeners);
+ }
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].subjectUpdated(subject, from);
+ }
+ }
+
+ /**
+ * Adds a new {@link PacketInterceptor} that will be invoked every time a new presence
+ * is going to be sent by this MultiUserChat to the server. Packet interceptors may
+ * add new extensions to the presence that is going to be sent to the MUC service.
+ *
+ * @param presenceInterceptor the new packet interceptor that will intercept presence packets.
+ */
+ public void addPresenceInterceptor(PacketInterceptor presenceInterceptor) {
+ presenceInterceptors.add(presenceInterceptor);
+ }
+
+ /**
+ * Removes a {@link PacketInterceptor} that was being invoked every time a new presence
+ * was being sent by this MultiUserChat to the server. Packet interceptors may
+ * add new extensions to the presence that is going to be sent to the MUC service.
+ *
+ * @param presenceInterceptor the packet interceptor to remove.
+ */
+ public void removePresenceInterceptor(PacketInterceptor presenceInterceptor) {
+ presenceInterceptors.remove(presenceInterceptor);
+ }
+
+ /**
+ * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room
+ * or the room does not have a subject yet. In case the room has a subject, as soon as the
+ * user joins the room a message with the current room's subject will be received.<p>
+ *
+ * To be notified every time the room's subject change you should add a listener
+ * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
+ *
+ * To change the room's subject use {@link #changeSubject(String)}.
+ *
+ * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the
+ * room does not have a subject yet.
+ */
+ public String getSubject() {
+ return subject;
+ }
+
+ /**
+ * Returns the reserved room nickname for the user in the room. A user may have a reserved
+ * nickname, for example through explicit room registration or database integration. In such
+ * cases it may be desirable for the user to discover the reserved nickname before attempting
+ * to enter the room.
+ *
+ * @return the reserved room nickname or <tt>null</tt> if none.
+ */
+ public String getReservedNickname() {
+ try {
+ DiscoverInfo result =
+ ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
+ room,
+ "x-roomuser-item");
+ // Look for an Identity that holds the reserved nickname and return its name
+ for (Iterator identities = result.getIdentities(); identities.hasNext();) {
+ DiscoverInfo.Identity identity = (DiscoverInfo.Identity) identities.next();
+ return identity.getName();
+ }
+ // If no Identity was found then the user does not have a reserved room nickname
+ return null;
+ }
+ catch (XMPPException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Returns the nickname that was used to join the room, or <tt>null</tt> if not
+ * currently joined.
+ *
+ * @return the nickname currently being used.
+ */
+ public String getNickname() {
+ return nickname;
+ }
+
+ /**
+ * Changes the occupant's nickname to a new nickname within the room. Each room occupant
+ * will receive two presence packets. One of type "unavailable" for the old nickname and one
+ * indicating availability for the new nickname. The unavailable presence will contain the new
+ * nickname and an appropriate status code (namely 303) as extended presence information. The
+ * status code 303 indicates that the occupant is changing his/her nickname.
+ *
+ * @param nickname the new nickname within the room.
+ * @throws XMPPException if the new nickname is already in use by another occupant.
+ */
+ public void changeNickname(String nickname) throws XMPPException {
+ if (nickname == null || nickname.equals("")) {
+ throw new IllegalArgumentException("Nickname must not be null or blank.");
+ }
+ // Check that we already have joined the room before attempting to change the
+ // nickname.
+ if (!joined) {
+ throw new IllegalStateException("Must be logged into the room to change nickname.");
+ }
+ // We change the nickname by sending a presence packet where the "to"
+ // field is in the form "roomName@service/nickname"
+ // We don't have to signal the MUC support again
+ Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
+ joinPresence.setTo(room + "/" + nickname);
+ // Invoke presence interceptors so that extra information can be dynamically added
+ for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) {
+ PacketInterceptor packetInterceptor = (PacketInterceptor) it.next();
+ packetInterceptor.interceptPacket(joinPresence);
+ }
+
+ // Wait for a presence packet back from the server.
+ PacketFilter responseFilter =
+ new AndFilter(
+ new FromMatchesFilter(room + "/" + nickname),
+ new PacketTypeFilter(Presence.class));
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send join packet.
+ connection.sendPacket(joinPresence);
+ // Wait up to a certain number of seconds for a reply.
+ Presence presence =
+ (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (presence == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+ this.nickname = nickname;
+ }
+
+ /**
+ * Changes the occupant's availability status within the room. The presence type
+ * will remain available but with a new status that describes the presence update and
+ * a new presence mode (e.g. Extended away).
+ *
+ * @param status a text message describing the presence update.
+ * @param mode the mode type for the presence update.
+ */
+ public void changeAvailabilityStatus(String status, Presence.Mode mode) {
+ if (nickname == null || nickname.equals("")) {
+ throw new IllegalArgumentException("Nickname must not be null or blank.");
+ }
+ // Check that we already have joined the room before attempting to change the
+ // availability status.
+ if (!joined) {
+ throw new IllegalStateException(
+ "Must be logged into the room to change the " + "availability status.");
+ }
+ // We change the availability status by sending a presence packet to the room with the
+ // new presence status and mode
+ Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
+ joinPresence.setStatus(status);
+ joinPresence.setMode(mode);
+ joinPresence.setTo(room + "/" + nickname);
+ // Invoke presence interceptors so that extra information can be dynamically added
+ for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) {
+ PacketInterceptor packetInterceptor = (PacketInterceptor) it.next();
+ packetInterceptor.interceptPacket(joinPresence);
+ }
+
+ // Send join packet.
+ connection.sendPacket(joinPresence);
+ }
+
+ /**
+ * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
+ * of type "unavailable" including a status code 307 and optionally along with the reason
+ * (if provided) and the bare JID of the user who initiated the kick. After the occupant
+ * was kicked from the room, the rest of the occupants will receive a presence of type
+ * "unavailable". The presence will include a status code 307 which means that the occupant
+ * was kicked from the room.
+ *
+ * @param nickname the nickname of the participant or visitor to kick from the room
+ * (e.g. "john").
+ * @param reason the reason why the participant or visitor is being kicked from the room.
+ * @throws XMPPException if an error occurs kicking the occupant. In particular, a
+ * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+ * was intended to be kicked (i.e. Not Allowed error); or a
+ * 403 error can occur if the occupant that intended to kick another occupant does
+ * not have kicking privileges (i.e. Forbidden error); or a
+ * 400 error can occur if the provided nickname is not present in the room.
+ */
+ public void kickParticipant(String nickname, String reason) throws XMPPException {
+ changeRole(nickname, "none", reason);
+ }
+
+ /**
+ * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
+ * who does and does not have "voice" in the room. To have voice means that a room occupant
+ * is able to send messages to the room occupants.
+ *
+ * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
+ * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
+ * 403 error can occur if the occupant that intended to grant voice is not
+ * a moderator in this room (i.e. Forbidden error); or a
+ * 400 error can occur if the provided nickname is not present in the room.
+ */
+ public void grantVoice(Collection nicknames) throws XMPPException {
+ changeRole(nicknames, "participant");
+ }
+
+ /**
+ * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
+ * who does and does not have "voice" in the room. To have voice means that a room occupant
+ * is able to send messages to the room occupants.
+ *
+ * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
+ * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
+ * 403 error can occur if the occupant that intended to grant voice is not
+ * a moderator in this room (i.e. Forbidden error); or a
+ * 400 error can occur if the provided nickname is not present in the room.
+ */
+ public void grantVoice(String nickname) throws XMPPException {
+ changeRole(nickname, "participant", null);
+ }
+
+ /**
+ * Revokes voice from participants in the room. In a moderated room, a moderator may want to
+ * revoke an occupant's privileges to speak. To have voice means that a room occupant
+ * is able to send messages to the room occupants.
+ *
+ * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
+ * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
+ * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+ * was tried to revoke his voice (i.e. Not Allowed error); or a
+ * 400 error can occur if the provided nickname is not present in the room.
+ */
+ public void revokeVoice(Collection nicknames) throws XMPPException {
+ changeRole(nicknames, "visitor");
+ }
+
+ /**
+ * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
+ * revoke an occupant's privileges to speak. To have voice means that a room occupant
+ * is able to send messages to the room occupants.
+ *
+ * @param nickname the nickname of the participant to revoke voice (e.g. "john").
+ * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
+ * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+ * was tried to revoke his voice (i.e. Not Allowed error); or a
+ * 400 error can occur if the provided nickname is not present in the room.
+ */
+ public void revokeVoice(String nickname) throws XMPPException {
+ changeRole(nickname, "visitor", null);
+ }
+
+ /**
+ * Bans users from the room. An admin or owner of the room can ban users from a room. This
+ * means that the banned user will no longer be able to join the room unless the ban has been
+ * removed. If the banned user was present in the room then he/she will be removed from the
+ * room and notified that he/she was banned along with the reason (if provided) and the bare
+ * XMPP user ID of the user who initiated the ban.
+ *
+ * @param jids the bare XMPP user IDs of the users to ban.
+ * @throws XMPPException if an error occurs banning a user. In particular, a
+ * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+ * was tried to be banned (i.e. Not Allowed error).
+ */
+ public void banUsers(Collection jids) throws XMPPException {
+ changeAffiliationByAdmin(jids, "outcast");
+ }
+
+ /**
+ * Bans a user from the room. An admin or owner of the room can ban users from a room. This
+ * means that the banned user will no longer be able to join the room unless the ban has been
+ * removed. If the banned user was present in the room then he/she will be removed from the
+ * room and notified that he/she was banned along with the reason (if provided) and the bare
+ * XMPP user ID of the user who initiated the ban.
+ *
+ * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
+ * @param reason the reason why the user was banned.
+ * @throws XMPPException if an error occurs banning a user. In particular, a
+ * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+ * was tried to be banned (i.e. Not Allowed error).
+ */
+ public void banUser(String jid, String reason) throws XMPPException {
+ changeAffiliationByAdmin(jid, "outcast", reason);
+ }
+
+ /**
+ * Grants membership to other users. Only administrators are able to grant membership. A user
+ * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
+ * that a user cannot enter without being on the member list).
+ *
+ * @param jids the XMPP user IDs of the users to grant membership.
+ * @throws XMPPException if an error occurs granting membership to a user.
+ */
+ public void grantMembership(Collection jids) throws XMPPException {
+ changeAffiliationByAdmin(jids, "member");
+ }
+
+ /**
+ * Grants membership to a user. Only administrators are able to grant membership. A user
+ * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
+ * that a user cannot enter without being on the member list).
+ *
+ * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
+ * @throws XMPPException if an error occurs granting membership to a user.
+ */
+ public void grantMembership(String jid) throws XMPPException {
+ changeAffiliationByAdmin(jid, "member", null);
+ }
+
+ /**
+ * Revokes users' membership. Only administrators are able to revoke membership. A user
+ * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
+ * that a user cannot enter without being on the member list). If the user is in the room and
+ * the room is of type members-only then the user will be removed from the room.
+ *
+ * @param jids the bare XMPP user IDs of the users to revoke membership.
+ * @throws XMPPException if an error occurs revoking membership to a user.
+ */
+ public void revokeMembership(Collection jids) throws XMPPException {
+ changeAffiliationByAdmin(jids, "none");
+ }
+
+ /**
+ * Revokes a user's membership. Only administrators are able to revoke membership. A user
+ * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
+ * that a user cannot enter without being on the member list). If the user is in the room and
+ * the room is of type members-only then the user will be removed from the room.
+ *
+ * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
+ * @throws XMPPException if an error occurs revoking membership to a user.
+ */
+ public void revokeMembership(String jid) throws XMPPException {
+ changeAffiliationByAdmin(jid, "none", null);
+ }
+
+ /**
+ * Grants moderator privileges to participants or visitors. Room administrators may grant
+ * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
+ * other users, modify room's subject plus all the partcipants privileges.
+ *
+ * @param nicknames the nicknames of the occupants to grant moderator privileges.
+ * @throws XMPPException if an error occurs granting moderator privileges to a user.
+ */
+ public void grantModerator(Collection nicknames) throws XMPPException {
+ changeRole(nicknames, "moderator");
+ }
+
+ /**
+ * Grants moderator privileges to a participant or visitor. Room administrators may grant
+ * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
+ * other users, modify room's subject plus all the partcipants privileges.
+ *
+ * @param nickname the nickname of the occupant to grant moderator privileges.
+ * @throws XMPPException if an error occurs granting moderator privileges to a user.
+ */
+ public void grantModerator(String nickname) throws XMPPException {
+ changeRole(nickname, "moderator", null);
+ }
+
+ /**
+ * Revokes moderator privileges from other users. The occupant that loses moderator
+ * privileges will become a participant. Room administrators may revoke moderator privileges
+ * only to occupants whose affiliation is member or none. This means that an administrator is
+ * not allowed to revoke moderator privileges from other room administrators or owners.
+ *
+ * @param nicknames the nicknames of the occupants to revoke moderator privileges.
+ * @throws XMPPException if an error occurs revoking moderator privileges from a user.
+ */
+ public void revokeModerator(Collection nicknames) throws XMPPException {
+ changeRole(nicknames, "participant");
+ }
+
+ /**
+ * Revokes moderator privileges from another user. The occupant that loses moderator
+ * privileges will become a participant. Room administrators may revoke moderator privileges
+ * only to occupants whose affiliation is member or none. This means that an administrator is
+ * not allowed to revoke moderator privileges from other room administrators or owners.
+ *
+ * @param nickname the nickname of the occupant to revoke moderator privileges.
+ * @throws XMPPException if an error occurs revoking moderator privileges from a user.
+ */
+ public void revokeModerator(String nickname) throws XMPPException {
+ changeRole(nickname, "participant", null);
+ }
+
+ /**
+ * Grants ownership privileges to other users. Room owners may grant ownership privileges.
+ * Some room implementations will not allow to grant ownership privileges to other users.
+ * An owner is allowed to change defining room features as well as perform all administrative
+ * functions.
+ *
+ * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
+ * @throws XMPPException if an error occurs granting ownership privileges to a user.
+ */
+ public void grantOwnership(Collection jids) throws XMPPException {
+ changeAffiliationByOwner(jids, "owner");
+ }
+
+ /**
+ * Grants ownership privileges to another user. Room owners may grant ownership privileges.
+ * Some room implementations will not allow to grant ownership privileges to other users.
+ * An owner is allowed to change defining room features as well as perform all administrative
+ * functions.
+ *
+ * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
+ * @throws XMPPException if an error occurs granting ownership privileges to a user.
+ */
+ public void grantOwnership(String jid) throws XMPPException {
+ changeAffiliationByOwner(jid, "owner");
+ }
+
+ /**
+ * Revokes ownership privileges from other users. The occupant that loses ownership
+ * privileges will become an administrator. Room owners may revoke ownership privileges.
+ * Some room implementations will not allow to grant ownership privileges to other users.
+ *
+ * @param jids the bare XMPP user IDs of the users to revoke ownership.
+ * @throws XMPPException if an error occurs revoking ownership privileges from a user.
+ */
+ public void revokeOwnership(Collection jids) throws XMPPException {
+ changeAffiliationByOwner(jids, "admin");
+ }
+
+ /**
+ * Revokes ownership privileges from another user. The occupant that loses ownership
+ * privileges will become an administrator. Room owners may revoke ownership privileges.
+ * Some room implementations will not allow to grant ownership privileges to other users.
+ *
+ * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
+ * @throws XMPPException if an error occurs revoking ownership privileges from a user.
+ */
+ public void revokeOwnership(String jid) throws XMPPException {
+ changeAffiliationByOwner(jid, "admin");
+ }
+
+ /**
+ * Grants administrator privileges to other users. Room owners may grant administrator
+ * privileges to a member or unaffiliated user. An administrator is allowed to perform
+ * administrative functions such as banning users and edit moderator list.
+ *
+ * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
+ * @throws XMPPException if an error occurs granting administrator privileges to a user.
+ */
+ public void grantAdmin(Collection jids) throws XMPPException {
+ changeAffiliationByOwner(jids, "admin");
+ }
+
+ /**
+ * Grants administrator privileges to another user. Room owners may grant administrator
+ * privileges to a member or unaffiliated user. An administrator is allowed to perform
+ * administrative functions such as banning users and edit moderator list.
+ *
+ * @param jid the bare XMPP user ID of the user to grant administrator privileges
+ * (e.g. "user@host.org").
+ * @throws XMPPException if an error occurs granting administrator privileges to a user.
+ */
+ public void grantAdmin(String jid) throws XMPPException {
+ changeAffiliationByOwner(jid, "admin");
+ }
+
+ /**
+ * Revokes administrator privileges from users. The occupant that loses administrator
+ * privileges will become a member. Room owners may revoke administrator privileges from
+ * a member or unaffiliated user.
+ *
+ * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
+ * @throws XMPPException if an error occurs revoking administrator privileges from a user.
+ */
+ public void revokeAdmin(Collection jids) throws XMPPException {
+ changeAffiliationByOwner(jids, "member");
+ }
+
+ /**
+ * Revokes administrator privileges from a user. The occupant that loses administrator
+ * privileges will become a member. Room owners may revoke administrator privileges from
+ * a member or unaffiliated user.
+ *
+ * @param jid the bare XMPP user ID of the user to revoke administrator privileges
+ * (e.g. "user@host.org").
+ * @throws XMPPException if an error occurs revoking administrator privileges from a user.
+ */
+ public void revokeAdmin(String jid) throws XMPPException {
+ changeAffiliationByOwner(jid, "member");
+ }
+
+ private void changeAffiliationByOwner(String jid, String affiliation) throws XMPPException {
+ MUCOwner iq = new MUCOwner();
+ iq.setTo(room);
+ iq.setType(IQ.Type.SET);
+ // Set the new affiliation.
+ MUCOwner.Item item = new MUCOwner.Item(affiliation);
+ item.setJid(jid);
+ iq.addItem(item);
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the change request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ private void changeAffiliationByOwner(Collection jids, String affiliation)
+ throws XMPPException {
+ MUCOwner iq = new MUCOwner();
+ iq.setTo(room);
+ iq.setType(IQ.Type.SET);
+ for (Iterator it=jids.iterator(); it.hasNext();) {
+ // Set the new affiliation.
+ MUCOwner.Item item = new MUCOwner.Item(affiliation);
+ item.setJid((String) it.next());
+ iq.addItem(item);
+ }
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the change request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ private void changeAffiliationByAdmin(String jid, String affiliation, String reason)
+ throws XMPPException {
+ MUCAdmin iq = new MUCAdmin();
+ iq.setTo(room);
+ iq.setType(IQ.Type.SET);
+ // Set the new affiliation.
+ MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
+ item.setJid(jid);
+ item.setReason(reason);
+ iq.addItem(item);
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the change request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ private void changeAffiliationByAdmin(Collection jids, String affiliation)
+ throws XMPPException {
+ MUCAdmin iq = new MUCAdmin();
+ iq.setTo(room);
+ iq.setType(IQ.Type.SET);
+ for (Iterator it=jids.iterator(); it.hasNext();) {
+ // Set the new affiliation.
+ MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
+ item.setJid((String) it.next());
+ iq.addItem(item);
+ }
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the change request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ private void changeRole(String nickname, String role, String reason) throws XMPPException {
+ MUCAdmin iq = new MUCAdmin();
+ iq.setTo(room);
+ iq.setType(IQ.Type.SET);
+ // Set the new role.
+ MUCAdmin.Item item = new MUCAdmin.Item(null, role);
+ item.setNick(nickname);
+ item.setReason(reason);
+ iq.addItem(item);
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the change request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ private void changeRole(Collection nicknames, String role) throws XMPPException {
+ MUCAdmin iq = new MUCAdmin();
+ iq.setTo(room);
+ iq.setType(IQ.Type.SET);
+ for (Iterator it=nicknames.iterator(); it.hasNext();) {
+ // Set the new role.
+ MUCAdmin.Item item = new MUCAdmin.Item(null, role);
+ item.setNick((String) it.next());
+ iq.addItem(item);
+ }
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the change request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ /**
+ * Returns the number of occupants in the group chat.<p>
+ *
+ * Note: this value will only be accurate after joining the group chat, and
+ * may fluctuate over time. If you query this value directly after joining the
+ * group chat it may not be accurate, as it takes a certain amount of time for
+ * the server to send all presence packets to this client.
+ *
+ * @return the number of occupants in the group chat.
+ */
+ public int getOccupantsCount() {
+ synchronized (occupantsMap) {
+ return occupantsMap.size();
+ }
+ }
+
+ /**
+ * Returns an Iterator (of Strings) for the list of fully qualified occupants
+ * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
+ * Typically, a client would only display the nickname of the occupant. To
+ * get the nickname from the fully qualified name, use the
+ * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method.
+ * Note: this value will only be accurate after joining the group chat, and may
+ * fluctuate over time.
+ *
+ * @return an Iterator for the occupants in the group chat.
+ */
+ public Iterator getOccupants() {
+ synchronized (occupantsMap) {
+ return Collections.unmodifiableList(new ArrayList(occupantsMap.keySet())).iterator();
+ }
+ }
+
+ /**
+ * Returns the presence info for a particular user, or <tt>null</tt> if the user
+ * is not in the room.<p>
+ *
+ * @param user the room occupant to search for his presence. The format of user must
+ * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
+ * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable
+ * or if no presence information is available.
+ */
+ public Presence getOccupantPresence(String user) {
+ return (Presence) occupantsMap.get(user);
+ }
+
+ /**
+ * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the
+ * user is not in the room. The Occupant object may include information such as full
+ * JID of the user as well as the role and affiliation of the user in the room.<p>
+ *
+ * @param user the room occupant to search for his presence. The format of user must
+ * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
+ * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room).
+ */
+ public Occupant getOccupant(String user) {
+ Presence presence = (Presence) occupantsMap.get(user);
+ if (presence != null) {
+ return new Occupant(presence);
+ }
+ return null;
+ }
+
+ /**
+ * Adds a packet listener that will be notified of any new Presence packets
+ * sent to the group chat. Using a listener is a suitable way to know when the list
+ * of occupants should be re-loaded due to any changes.
+ *
+ * @param listener a packet listener that will be notified of any presence packets
+ * sent to the group chat.
+ */
+ public void addParticipantListener(PacketListener listener) {
+ connection.addPacketListener(listener, presenceFilter);
+ connectionListeners.add(listener);
+ }
+
+ /**
+ * Remoces a packet listener that was being notified of any new Presence packets
+ * sent to the group chat.
+ *
+ * @param listener a packet listener that was being notified of any presence packets
+ * sent to the group chat.
+ */
+ public void removeParticipantListener(PacketListener listener) {
+ connection.removePacketListener(listener);
+ connectionListeners.remove(listener);
+ }
+
+ /**
+ * Returns a collection of <code>Affiliate</code> with the room owners.
+ *
+ * @return a collection of <code>Affiliate</code> with the room owners.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ public Collection getOwners() throws XMPPException {
+ return getAffiliatesByOwner("owner");
+ }
+
+ /**
+ * Returns a collection of <code>Affiliate</code> with the room administrators.
+ *
+ * @return a collection of <code>Affiliate</code> with the room administrators.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ public Collection getAdmins() throws XMPPException {
+ return getAffiliatesByOwner("admin");
+ }
+
+ /**
+ * Returns a collection of <code>Affiliate</code> with the room members.
+ *
+ * @return a collection of <code>Affiliate</code> with the room members.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ public Collection getMembers() throws XMPPException {
+ return getAffiliatesByAdmin("member");
+ }
+
+ /**
+ * Returns a collection of <code>Affiliate</code> with the room outcasts.
+ *
+ * @return a collection of <code>Affiliate</code> with the room outcasts.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ public Collection getOutcasts() throws XMPPException {
+ return getAffiliatesByAdmin("outcast");
+ }
+
+ /**
+ * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
+ * sending a request in the owner namespace.
+ *
+ * @param affiliation the affiliation of the users in the room.
+ * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ private Collection getAffiliatesByOwner(String affiliation) throws XMPPException {
+ MUCOwner iq = new MUCOwner();
+ iq.setTo(room);
+ iq.setType(IQ.Type.GET);
+ // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
+ MUCOwner.Item item = new MUCOwner.Item(affiliation);
+ iq.addItem(item);
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ // Get the list of affiliates from the server's answer
+ List affiliates = new ArrayList();
+ for (Iterator it = answer.getItems(); it.hasNext();) {
+ affiliates.add(new Affiliate((MUCOwner.Item) it.next()));
+ }
+ return affiliates;
+ }
+
+ /**
+ * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
+ * sending a request in the admin namespace.
+ *
+ * @param affiliation the affiliation of the users in the room.
+ * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ private Collection getAffiliatesByAdmin(String affiliation) throws XMPPException {
+ MUCAdmin iq = new MUCAdmin();
+ iq.setTo(room);
+ iq.setType(IQ.Type.GET);
+ // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
+ MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
+ iq.addItem(item);
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ // Get the list of affiliates from the server's answer
+ List affiliates = new ArrayList();
+ for (Iterator it = answer.getItems(); it.hasNext();) {
+ affiliates.add(new Affiliate((MUCAdmin.Item) it.next()));
+ }
+ return affiliates;
+ }
+
+ /**
+ * Returns a collection of <code>Occupant</code> with the room moderators.
+ *
+ * @return a collection of <code>Occupant</code> with the room moderators.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ public Collection getModerators() throws XMPPException {
+ return getOccupants("moderator");
+ }
+
+ /**
+ * Returns a collection of <code>Occupant</code> with the room participants.
+ *
+ * @return a collection of <code>Occupant</code> with the room participants.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ public Collection getParticipants() throws XMPPException {
+ return getOccupants("participant");
+ }
+
+ /**
+ * Returns a collection of <code>Occupant</code> that have the specified room role.
+ *
+ * @param role the role of the occupant in the room.
+ * @return a collection of <code>Occupant</code> that have the specified room role.
+ * @throws XMPPException if an error occured while performing the request to the server or you
+ * don't have enough privileges to get this information.
+ */
+ private Collection getOccupants(String role) throws XMPPException {
+ MUCAdmin iq = new MUCAdmin();
+ iq.setTo(room);
+ iq.setType(IQ.Type.GET);
+ // Set the specified role. This may request the list of moderators/participants.
+ MUCAdmin.Item item = new MUCAdmin.Item(null, role);
+ iq.addItem(item);
+
+ // Wait for a response packet back from the server.
+ PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send the request to the server.
+ connection.sendPacket(iq);
+ // Wait up to a certain number of seconds for a reply.
+ MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ // Get the list of participants from the server's answer
+ List participants = new ArrayList();
+ for (Iterator it = answer.getItems(); it.hasNext();) {
+ participants.add(new Occupant((MUCAdmin.Item) it.next()));
+ }
+ return participants;
+ }
+
+ /**
+ * Sends a message to the chat room.
+ *
+ * @param text the text of the message to send.
+ * @throws XMPPException if sending the message fails.
+ */
+ public void sendMessage(String text) throws XMPPException {
+ Message message = new Message(room, Message.Type.GROUP_CHAT);
+ message.setBody(text);
+ connection.sendPacket(message);
+ }
+
+ /**
+ * Returns a new Chat for sending private messages to a given room occupant.
+ * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
+ * service will change the 'from' address to the sender's room JID and delivering the message
+ * to the intended recipient's full JID.
+ *
+ * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
+ * @return new Chat for sending private messages to a given room occupant.
+ */
+ public Chat createPrivateChat(String occupant) {
+ return new Chat(connection, occupant);
+ }
+
+ /**
+ * Creates a new Message to send to the chat room.
+ *
+ * @return a new Message addressed to the chat room.
+ */
+ public Message createMessage() {
+ return new Message(room, Message.Type.GROUP_CHAT);
+ }
+
+ /**
+ * Sends a Message to the chat room.
+ *
+ * @param message the message.
+ * @throws XMPPException if sending the message fails.
+ */
+ public void sendMessage(Message message) throws XMPPException {
+ connection.sendPacket(message);
+ }
+
+ /**
+ * Polls for and returns the next message, or <tt>null</tt> if there isn't
+ * a message immediately available. This method provides significantly different
+ * functionalty than the {@link #nextMessage()} method since it's non-blocking.
+ * In other words, the method call will always return immediately, whereas the
+ * nextMessage method will return only when a message is available (or after
+ * a specific timeout).
+ *
+ * @return the next message if one is immediately available and
+ * <tt>null</tt> otherwise.
+ */
+ public Message pollMessage() {
+ return (Message) messageCollector.pollResult();
+ }
+
+ /**
+ * Returns the next available message in the chat. The method call will block
+ * (not return) until a message is available.
+ *
+ * @return the next message.
+ */
+ public Message nextMessage() {
+ return (Message) messageCollector.nextResult();
+ }
+
+ /**
+ * Returns the next available message in the chat. The method call will block
+ * (not return) until a packet is available or the <tt>timeout</tt> has elapased.
+ * If the timeout elapses without a result, <tt>null</tt> will be returned.
+ *
+ * @param timeout the maximum amount of time to wait for the next message.
+ * @return the next message, or <tt>null</tt> if the timeout elapses without a
+ * message becoming available.
+ */
+ public Message nextMessage(long timeout) {
+ return (Message) messageCollector.nextResult(timeout);
+ }
+
+ /**
+ * Adds a packet listener that will be notified of any new messages in the
+ * group chat. Only "group chat" messages addressed to this group chat will
+ * be delivered to the listener. If you wish to listen for other packets
+ * that may be associated with this group chat, you should register a
+ * PacketListener directly with the XMPPConnection with the appropriate
+ * PacketListener.
+ *
+ * @param listener a packet listener.
+ */
+ public void addMessageListener(PacketListener listener) {
+ connection.addPacketListener(listener, messageFilter);
+ connectionListeners.add(listener);
+ }
+
+ /**
+ * Removes a packet listener that was being notified of any new messages in the
+ * multi user chat. Only "group chat" messages addressed to this multi user chat were
+ * being delivered to the listener.
+ *
+ * @param listener a packet listener.
+ */
+ public void removeMessageListener(PacketListener listener) {
+ connection.removePacketListener(listener);
+ connectionListeners.remove(listener);
+ }
+
+ /**
+ * Changes the subject within the room. As a default, only users with a role of "moderator"
+ * are allowed to change the subject in a room. Although some rooms may be configured to
+ * allow a mere participant or even a visitor to change the subject.
+ *
+ * @param subject the new room's subject to set.
+ * @throws XMPPException if someone without appropriate privileges attempts to change the
+ * room subject will throw an error with code 403 (i.e. Forbidden)
+ */
+ public void changeSubject(final String subject) throws XMPPException {
+ Message message = new Message(room, Message.Type.GROUP_CHAT);
+ message.setSubject(subject);
+ // Wait for an error or confirmation message back from the server.
+ PacketFilter responseFilter =
+ new AndFilter(
+ new FromMatchesFilter(room),
+ new PacketTypeFilter(Message.class));
+ responseFilter = new AndFilter(responseFilter, new PacketFilter() {
+ public boolean accept(Packet packet) {
+ Message msg = (Message) packet;
+ return subject.equals(msg.getSubject());
+ }
+ });
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+ // Send change subject packet.
+ connection.sendPacket(message);
+ // Wait up to a certain number of seconds for a reply.
+ Message answer =
+ (Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ response.cancel();
+
+ if (answer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (answer.getError() != null) {
+ throw new XMPPException(answer.getError());
+ }
+ }
+
+ /**
+ * Notification message that the user has joined the room.
+ */
+ private synchronized void userHasJoined() {
+ // Update the list of joined rooms through this connection
+ ArrayList rooms = (ArrayList)joinedRooms.get(connection);
+ if (rooms == null) {
+ rooms = new ArrayList();
+ joinedRooms.put(connection, rooms);
+ }
+ rooms.add(room);
+ }
+
+ /**
+ * Notification message that the user has left the room.
+ */
+ private synchronized void userHasLeft() {
+ // Update the list of joined rooms through this connection
+ ArrayList rooms = (ArrayList)joinedRooms.get(connection);
+ if (rooms == null) {
+ return;
+ }
+ rooms.remove(room);
+ }
+
+ /**
+ * Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none.
+ *
+ * @param packet the packet that may include the MUCUser extension.
+ * @return the MUCUser found in the packet.
+ */
+ private MUCUser getMUCUserExtension(Packet packet) {
+ if (packet != null) {
+ // Get the MUC User extension
+ return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
+ }
+ return null;
+ }
+
+ /**
+ * Adds a listener that will be notified of changes in your status in the room
+ * such as the user being kicked, banned, or granted admin permissions.
+ *
+ * @param listener a user status listener.
+ */
+ public void addUserStatusListener(UserStatusListener listener) {
+ synchronized (userStatusListeners) {
+ if (!userStatusListeners.contains(listener)) {
+ userStatusListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener that was being notified of changes in your status in the room
+ * such as the user being kicked, banned, or granted admin permissions.
+ *
+ * @param listener a user status listener.
+ */
+ public void removeUserStatusListener(UserStatusListener listener) {
+ synchronized (userStatusListeners) {
+ userStatusListeners.remove(listener);
+ }
+ }
+
+ private void fireUserStatusListeners(String methodName, Object[] params) {
+ UserStatusListener[] listeners = null;
+ synchronized (userStatusListeners) {
+ listeners = new UserStatusListener[userStatusListeners.size()];
+ userStatusListeners.toArray(listeners);
+ }
+ // Get the classes of the method parameters
+ Class[] paramClasses = new Class[params.length];
+ for (int i = 0; i < params.length; i++) {
+ paramClasses[i] = params[i].getClass();
+ }
+ try {
+ // Get the method to execute based on the requested methodName and parameters classes
+ Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses);
+ for (int i = 0; i < listeners.length; i++) {
+ method.invoke(listeners[i], params);
+ }
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Adds a listener that will be notified of changes in occupants status in the room
+ * such as the user being kicked, banned, or granted admin permissions.
+ *
+ * @param listener a participant status listener.
+ */
+ public void addParticipantStatusListener(ParticipantStatusListener listener) {
+ synchronized (participantStatusListeners) {
+ if (!participantStatusListeners.contains(listener)) {
+ participantStatusListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener that was being notified of changes in occupants status in the room
+ * such as the user being kicked, banned, or granted admin permissions.
+ *
+ * @param listener a participant status listener.
+ */
+ public void removeParticipantStatusListener(ParticipantStatusListener listener) {
+ synchronized (participantStatusListeners) {
+ participantStatusListeners.remove(listener);
+ }
+ }
+
+ private void fireParticipantStatusListeners(String methodName, List params) {
+ ParticipantStatusListener[] listeners = null;
+ synchronized (participantStatusListeners) {
+ listeners = new ParticipantStatusListener[participantStatusListeners.size()];
+ participantStatusListeners.toArray(listeners);
+ }
+ try {
+ // Get the method to execute based on the requested methodName and parameter
+ Class[] classes = new Class[params.size()];
+ for (int i=0;i<params.size(); i++) {
+ classes[i] = String.class;
+ }
+ Method method = ParticipantStatusListener.class.getDeclaredMethod(methodName, classes);
+ for (int i = 0; i < listeners.length; i++) {
+ method.invoke(listeners[i], params.toArray());
+ }
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void init() {
+ // Create a collector for incoming messages.
+ messageFilter =
+ new AndFilter(
+ new FromMatchesFilter(room),
+ new MessageTypeFilter(Message.Type.GROUP_CHAT));
+ messageFilter = new AndFilter(messageFilter, new PacketFilter() {
+ public boolean accept(Packet packet) {
+ Message msg = (Message) packet;
+ return msg.getBody() != null;
+ }
+ });
+ messageCollector = connection.createPacketCollector(messageFilter);
+
+ // Create a listener for subject updates.
+ subjectFilter =
+ new AndFilter(
+ new FromMatchesFilter(room),
+ new MessageTypeFilter(Message.Type.GROUP_CHAT));
+ subjectFilter = new AndFilter(subjectFilter, new PacketFilter() {
+ public boolean accept(Packet packet) {
+ Message msg = (Message) packet;
+ return msg.getSubject() != null;
+ }
+ });
+ subjectListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ Message msg = (Message) packet;
+ // Update the room subject
+ subject = msg.getSubject();
+ // Fire event for subject updated listeners
+ fireSubjectUpdatedListeners(
+ msg.getSubject(),
+ msg.getFrom());
+
+ }
+ };
+ connection.addPacketListener(subjectListener, subjectFilter);
+
+ // Create a listener for all presence updates.
+ presenceFilter =
+ new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class));
+ presenceListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ Presence presence = (Presence) packet;
+ String from = presence.getFrom();
+ String myRoomJID = room + "/" + nickname;
+ boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
+ if (presence.getType() == Presence.Type.AVAILABLE) {
+ Presence oldPresence;
+ synchronized (occupantsMap) {
+ oldPresence = (Presence)occupantsMap.get(from);
+ occupantsMap.put(from, presence);
+ }
+ if (oldPresence != null) {
+ // Get the previous occupant's affiliation & role
+ MUCUser mucExtension = getMUCUserExtension(oldPresence);
+ String oldAffiliation = mucExtension.getItem().getAffiliation();
+ String oldRole = mucExtension.getItem().getRole();
+ // Get the new occupant's affiliation & role
+ mucExtension = getMUCUserExtension(presence);
+ String newAffiliation = mucExtension.getItem().getAffiliation();
+ String newRole = mucExtension.getItem().getRole();
+ // Fire role modification events
+ checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
+ // Fire affiliation modification events
+ checkAffiliationModifications(
+ oldAffiliation,
+ newAffiliation,
+ isUserStatusModification,
+ from);
+ }
+ else {
+ // A new occupant has joined the room
+ if (!isUserStatusModification) {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("joined", params);
+ }
+ }
+ }
+ else if (presence.getType() == Presence.Type.UNAVAILABLE) {
+ synchronized (occupantsMap) {
+ occupantsMap.remove(from);
+ }
+ MUCUser mucUser = getMUCUserExtension(presence);
+ if (mucUser != null && mucUser.getStatus() != null) {
+ // Fire events according to the received presence code
+ checkPresenceCode(
+ mucUser.getStatus().getCode(),
+ presence.getFrom().equals(myRoomJID),
+ mucUser,
+ from);
+ } else {
+ // An occupant has left the room
+ if (!isUserStatusModification) {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("left", params);
+ }
+ }
+ }
+ }
+ };
+ connection.addPacketListener(presenceListener, presenceFilter);
+
+ // Listens for all messages that include a MUCUser extension and fire the invitation
+ // rejection listeners if the message includes an invitation rejection.
+ declinesFilter = new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
+ declinesListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ // Get the MUC User extension
+ MUCUser mucUser = getMUCUserExtension(packet);
+ // Check if the MUCUser informs that the invitee has declined the invitation
+ if (mucUser.getDecline() != null &&
+ ((Message) packet).getType() != Message.Type.ERROR) {
+ // Fire event for invitation rejection listeners
+ fireInvitationRejectionListeners(
+ mucUser.getDecline().getFrom(),
+ mucUser.getDecline().getReason());
+ }
+ };
+ };
+ connection.addPacketListener(declinesListener, declinesFilter);
+ }
+
+ /**
+ * Fires notification events if the role of a room occupant has changed. If the occupant that
+ * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
+ * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
+ * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
+ * <code>MultiUserChat</code> will be fired. The following table shows the events that will
+ * be fired depending on the previous and new role of the occupant.
+ *
+ * <pre>
+ * <table border="1">
+ * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
+ *
+ * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
+ * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
+ * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
+ *
+ * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
+ * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
+ * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
+ *
+ * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
+ * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
+ * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
+ *
+ * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
+ * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
+ * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
+ * </table>
+ * </pre>
+ *
+ * @param oldRole the previous role of the user in the room before receiving the new presence
+ * @param newRole the new role of the user in the room after receiving the new presence
+ * @param isUserModification whether the received presence is about your user in the room or not
+ * @param from the occupant whose role in the room has changed
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ private void checkRoleModifications(
+ String oldRole,
+ String newRole,
+ boolean isUserModification,
+ String from) {
+ // Voice was granted to a visitor
+ if (("visitor".equals(oldRole) || "none".equals(oldRole))
+ && "participant".equals(newRole)) {
+ if (isUserModification) {
+ fireUserStatusListeners("voiceGranted", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("voiceGranted", params);
+ }
+ }
+ // The participant's voice was revoked from the room
+ else if (
+ "participant".equals(oldRole)
+ && ("visitor".equals(newRole) || "none".equals(newRole))) {
+ if (isUserModification) {
+ fireUserStatusListeners("voiceRevoked", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("voiceRevoked", params);
+ }
+ }
+ // Moderator privileges were granted to a participant
+ if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) {
+ if ("visitor".equals(oldRole) || "none".equals(oldRole)) {
+ if (isUserModification) {
+ fireUserStatusListeners("voiceGranted", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("voiceGranted", params);
+ }
+ }
+ if (isUserModification) {
+ fireUserStatusListeners("moderatorGranted", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("moderatorGranted", params);
+ }
+ }
+ // Moderator privileges were revoked from a participant
+ else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) {
+ if ("visitor".equals(newRole) || "none".equals(newRole)) {
+ if (isUserModification) {
+ fireUserStatusListeners("voiceRevoked", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("voiceRevoked", params);
+ }
+ }
+ if (isUserModification) {
+ fireUserStatusListeners("moderatorRevoked", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("moderatorRevoked", params);
+ }
+ }
+ }
+
+ /**
+ * Fires notification events if the affiliation of a room occupant has changed. If the
+ * occupant that changed his affiliation is your occupant then the
+ * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
+ * On the other hand, if the occupant that changed his affiliation is not yours then the
+ * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
+ * fired. The following table shows the events that will be fired depending on the previous
+ * and new affiliation of the occupant.
+ *
+ * <pre>
+ * <table border="1">
+ * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
+ *
+ * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
+ * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
+ * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
+ *
+ * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
+ * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
+ * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
+ *
+ * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
+ * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
+ * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
+ *
+ * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
+ * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
+ * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
+ * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
+ * </table>
+ * </pre>
+ *
+ * @param oldAffiliation the previous affiliation of the user in the room before receiving the
+ * new presence
+ * @param newAffiliation the new affiliation of the user in the room after receiving the new
+ * presence
+ * @param isUserModification whether the received presence is about your user in the room or not
+ * @param from the occupant whose role in the room has changed
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ private void checkAffiliationModifications(
+ String oldAffiliation,
+ String newAffiliation,
+ boolean isUserModification,
+ String from) {
+ // First check for revoked affiliation and then for granted affiliations. The idea is to
+ // first fire the "revoke" events and then fire the "grant" events.
+
+ // The user's ownership to the room was revoked
+ if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) {
+ if (isUserModification) {
+ fireUserStatusListeners("ownershipRevoked", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("ownershipRevoked", params);
+ }
+ }
+ // The user's administrative privileges to the room were revoked
+ else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) {
+ if (isUserModification) {
+ fireUserStatusListeners("adminRevoked", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("adminRevoked", params);
+ }
+ }
+ // The user's membership to the room was revoked
+ else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) {
+ if (isUserModification) {
+ fireUserStatusListeners("membershipRevoked", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("membershipRevoked", params);
+ }
+ }
+
+ // The user was granted ownership to the room
+ if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) {
+ if (isUserModification) {
+ fireUserStatusListeners("ownershipGranted", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("ownershipGranted", params);
+ }
+ }
+ // The user was granted administrative privileges to the room
+ else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) {
+ if (isUserModification) {
+ fireUserStatusListeners("adminGranted", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("adminGranted", params);
+ }
+ }
+ // The user was granted membership to the room
+ else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) {
+ if (isUserModification) {
+ fireUserStatusListeners("membershipGranted", new Object[] {});
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ fireParticipantStatusListeners("membershipGranted", params);
+ }
+ }
+ }
+
+ /**
+ * Fires events according to the received presence code.
+ *
+ * @param code
+ * @param isUserModification
+ * @param mucUser
+ * @param from
+ */
+ private void checkPresenceCode(
+ String code,
+ boolean isUserModification,
+ MUCUser mucUser,
+ String from) {
+ // Check if an occupant was kicked from the room
+ if ("307".equals(code)) {
+ // Check if this occupant was kicked
+ if (isUserModification) {
+ joined = false;
+
+ fireUserStatusListeners(
+ "kicked",
+ new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
+
+ // Reset occupant information.
+ occupantsMap = new HashMap();
+ nickname = null;
+ userHasLeft();
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ params.add(mucUser.getItem().getActor());
+ params.add(mucUser.getItem().getReason());
+ fireParticipantStatusListeners("kicked", params);
+ }
+ }
+ // A user was banned from the room
+ else if ("301".equals(code)) {
+ // Check if this occupant was banned
+ if (isUserModification) {
+ joined = false;
+
+ fireUserStatusListeners(
+ "banned",
+ new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
+
+ // Reset occupant information.
+ occupantsMap = new HashMap();
+ nickname = null;
+ userHasLeft();
+ }
+ else {
+ List params = new ArrayList();
+ params.add(from);
+ params.add(mucUser.getItem().getActor());
+ params.add(mucUser.getItem().getReason());
+ fireParticipantStatusListeners("banned", params);
+ }
+ }
+ // A user's membership was revoked from the room
+ else if ("321".equals(code)) {
+ // Check if this occupant's membership was revoked
+ if (isUserModification) {
+ joined = false;
+
+ fireUserStatusListeners("membershipRevoked", new Object[] {});
+
+ // Reset occupant information.
+ occupantsMap = new HashMap();
+ nickname = null;
+ userHasLeft();
+ }
+ }
+ // A occupant has changed his nickname in the room
+ else if ("303".equals(code)) {
+ List params = new ArrayList();
+ params.add(from);
+ params.add(mucUser.getItem().getNick());
+ fireParticipantStatusListeners("nicknameChanged", params);
+ }
+ }
+
+ public void finalize() throws Throwable {
+ super.finalize();
+ try {
+ if (connection != null) {
+ messageCollector.cancel();
+ connection.removePacketListener(subjectListener);
+ connection.removePacketListener(presenceListener);
+ connection.removePacketListener(declinesListener);
+ // Remove all the PacketListeners added to the connection by this chat
+ for (Iterator it=connectionListeners.iterator(); it.hasNext();) {
+ connection.removePacketListener((PacketListener) it.next());
+ }
+ }
+ }
+ catch (Exception e) {}
+ }
+
+ /**
+ * An InvitationsMonitor monitors a given connection to detect room invitations. Every
+ * time the InvitationsMonitor detects a new invitation it will fire the invitation listeners.
+ *
+ * @author Gaston Dombiak
+ */
+ private static class InvitationsMonitor implements ConnectionListener {
+ // We use a WeakHashMap so that the GC can collect the monitor when the
+ // connection is no longer referenced by any object.
+ private static Map monitors = new WeakHashMap();
+
+ private List invitationsListeners = new ArrayList();
+ private XMPPConnection connection;
+ private PacketFilter invitationFilter;
+ private PacketListener invitationPacketListener;
+
+ /**
+ * Returns a new or existing InvitationsMonitor for a given connection.
+ *
+ * @param conn the connection to monitor for room invitations.
+ * @return a new or existing InvitationsMonitor for a given connection.
+ */
+ public static InvitationsMonitor getInvitationsMonitor(XMPPConnection conn) {
+ synchronized (monitors) {
+ if (!monitors.containsKey(conn)) {
+ // We need to use a WeakReference because the monitor references the
+ // connection and this could prevent the GC from collecting the monitor
+ // when no other object references the monitor
+ monitors.put(conn, new WeakReference(new InvitationsMonitor(conn)));
+ }
+ // Return the InvitationsMonitor that monitors the connection
+ return (InvitationsMonitor) ((WeakReference) monitors.get(conn)).get();
+ }
+ }
+
+ /**
+ * Creates a new InvitationsMonitor that will monitor invitations received
+ * on a given connection.
+ *
+ * @param connection the connection to monitor for possible room invitations
+ */
+ private InvitationsMonitor(XMPPConnection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Adds a listener to invitation notifications. The listener will be fired anytime
+ * an invitation is received.<p>
+ *
+ * If this is the first monitor's listener then the monitor will be initialized in
+ * order to start listening to room invitations.
+ *
+ * @param listener an invitation listener.
+ */
+ public void addInvitationListener(InvitationListener listener) {
+ synchronized (invitationsListeners) {
+ // If this is the first monitor's listener then initialize the listeners
+ // on the connection to detect room invitations
+ if (invitationsListeners.size() == 0) {
+ init();
+ }
+ if (!invitationsListeners.contains(listener)) {
+ invitationsListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener to invitation notifications. The listener will be fired anytime
+ * an invitation is received.<p>
+ *
+ * If there are no more listeners to notifiy for room invitations then the monitor will
+ * be stopped. As soon as a new listener is added to the monitor, the monitor will resume
+ * monitoring the connection for new room invitations.
+ *
+ * @param listener an invitation listener.
+ */
+ public void removeInvitationListener(InvitationListener listener) {
+ synchronized (invitationsListeners) {
+ if (invitationsListeners.contains(listener)) {
+ invitationsListeners.remove(listener);
+ }
+ // If there are no more listeners to notifiy for room invitations
+ // then proceed to cancel/release this monitor
+ if (invitationsListeners.size() == 0) {
+ cancel();
+ }
+ }
+ }
+
+ /**
+ * Fires invitation listeners.
+ */
+ private void fireInvitationListeners(String room, String inviter, String reason, String password,
+ Message message) {
+ InvitationListener[] listeners = null;
+ synchronized (invitationsListeners) {
+ listeners = new InvitationListener[invitationsListeners.size()];
+ invitationsListeners.toArray(listeners);
+ }
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].invitationReceived(connection, room, inviter, reason, password, message);
+ }
+ }
+
+ public void connectionClosed() {
+ cancel();
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ cancel();
+ }
+
+ /**
+ * Initializes the listeners to detect received room invitations and to detect when the
+ * connection gets closed. As soon as a room invitation is received the invitations
+ * listeners will be fired. When the connection gets closed the monitor will remove
+ * his listeners on the connection.
+ */
+ private void init() {
+ // Listens for all messages that include a MUCUser extension and fire the invitation
+ // listeners if the message includes an invitation.
+ invitationFilter =
+ new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
+ invitationPacketListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ // Get the MUCUser extension
+ MUCUser mucUser =
+ (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
+ // Check if the MUCUser extension includes an invitation
+ if (mucUser.getInvite() != null &&
+ ((Message) packet).getType() != Message.Type.ERROR) {
+ // Fire event for invitation listeners
+ fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(),
+ mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet);
+ }
+ };
+ };
+ connection.addPacketListener(invitationPacketListener, invitationFilter);
+ // Add a listener to detect when the connection gets closed in order to
+ // cancel/release this monitor
+ connection.addConnectionListener(this);
+ }
+
+ /**
+ * Cancels all the listeners that this InvitationsMonitor has added to the connection.
+ */
+ private void cancel() {
+ connection.removePacketListener(invitationPacketListener);
+ connection.removeConnectionListener(this);
+ }
+
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Occupant.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Occupant.java
new file mode 100644
index 000000000..e82de84ad
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Occupant.java
@@ -0,0 +1,104 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+import org.jivesoftware.smackx.packet.MUCAdmin;
+import org.jivesoftware.smackx.packet.MUCUser;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Represents the information about an occupant in a given room. The information will always have
+ * the affiliation and role of the occupant in the room. The full JID and nickname are optional.
+ *
+ * @author Gaston Dombiak
+ */
+public class Occupant {
+ // Fields that must have a value
+ private String affiliation;
+ private String role;
+ // Fields that may have a value
+ private String jid;
+ private String nick;
+
+ Occupant(MUCAdmin.Item item) {
+ super();
+ this.jid = item.getJid();
+ this.affiliation = item.getAffiliation();
+ this.role = item.getRole();
+ this.nick = item.getNick();
+ }
+
+ Occupant(Presence presence) {
+ super();
+ MUCUser mucUser = (MUCUser) presence.getExtension("x",
+ "http://jabber.org/protocol/muc#user");
+ MUCUser.Item item = mucUser.getItem();
+ this.jid = item.getJid();
+ this.affiliation = item.getAffiliation();
+ this.role = item.getRole();
+ // Get the nickname from the FROM attribute of the presence
+ this.nick = StringUtils.parseResource(presence.getFrom());
+ }
+
+ /**
+ * Returns the full JID of the occupant. If this information was extracted from a presence and
+ * the room is semi or full-anonymous then the answer will be null. On the other hand, if this
+ * information was obtained while maintaining the voice list or the moderator list then we will
+ * always have a full JID.
+ *
+ * @return the full JID of the occupant.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the affiliation of the occupant. Possible affiliations are: "owner", "admin",
+ * "member", "outcast". This information will always be available.
+ *
+ * @return the affiliation of the occupant.
+ */
+ public String getAffiliation() {
+ return affiliation;
+ }
+
+ /**
+ * Returns the current role of the occupant in the room. This information will always be
+ * available.
+ *
+ * @return the current role of the occupant in the room.
+ */
+ public String getRole() {
+ return role;
+ }
+
+ /**
+ * Returns the current nickname of the occupant in the room. If this information was extracted
+ * from a presence then the answer will be null.
+ *
+ * @return the current nickname of the occupant in the room or null if this information was
+ * obtained from a presence.
+ */
+ public String getNick() {
+ return nick;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java
new file mode 100644
index 000000000..4035b2804
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java
@@ -0,0 +1,179 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+/**
+ * A listener that is fired anytime a participant's status in a room is changed, such as the
+ * user being kicked, banned, or granted admin permissions.
+ *
+ * @author Gaston Dombiak
+ */
+public interface ParticipantStatusListener {
+
+ /**
+ * Called when a new room occupant has joined the room. Note: Take in consideration that when
+ * you join a room you will receive the list of current occupants in the room. This message will
+ * be sent for each occupant.
+ *
+ * @param participant the participant that has just joined the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void joined(String participant);
+
+ /**
+ * Called when a room occupant has left the room on its own. This means that the occupant was
+ * neither kicked nor banned from the room.
+ *
+ * @param participant the participant that has left the room on its own.
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void left(String participant);
+
+ /**
+ * Called when a room participant has been kicked from the room. This means that the kicked
+ * participant is no longer participating in the room.
+ *
+ * @param participant the participant that was kicked from the room
+ * (e.g. room@conference.jabber.org/nick).
+ * @param actor the moderator that kicked the occupant from the room (e.g. user@host.org).
+ * @param reason the reason provided by the actor to kick the occupant from the room.
+ */
+ public abstract void kicked(String participant, String actor, String reason);
+
+ /**
+ * Called when a moderator grants voice to a visitor. This means that the visitor
+ * can now participate in the moderated room sending messages to all occupants.
+ *
+ * @param participant the participant that was granted voice in the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void voiceGranted(String participant);
+
+ /**
+ * Called when a moderator revokes voice from a participant. This means that the participant
+ * in the room was able to speak and now is a visitor that can't send messages to the room
+ * occupants.
+ *
+ * @param participant the participant that was revoked voice from the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void voiceRevoked(String participant);
+
+ /**
+ * Called when an administrator or owner banned a participant from the room. This means that
+ * banned participant will no longer be able to join the room unless the ban has been removed.
+ *
+ * @param participant the participant that was banned from the room
+ * (e.g. room@conference.jabber.org/nick).
+ * @param actor the administrator that banned the occupant (e.g. user@host.org).
+ * @param reason the reason provided by the administrator to ban the occupant.
+ */
+ public abstract void banned(String participant, String actor, String reason);
+
+ /**
+ * Called when an administrator grants a user membership to the room. This means that the user
+ * will be able to join the members-only room.
+ *
+ * @param participant the participant that was granted membership in the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void membershipGranted(String participant);
+
+ /**
+ * Called when an administrator revokes a user membership to the room. This means that the
+ * user will not be able to join the members-only room.
+ *
+ * @param participant the participant that was revoked membership from the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void membershipRevoked(String participant);
+
+ /**
+ * Called when an administrator grants moderator privileges to a user. This means that the user
+ * will be able to kick users, grant and revoke voice, invite other users, modify room's
+ * subject plus all the partcipants privileges.
+ *
+ * @param participant the participant that was granted moderator privileges in the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void moderatorGranted(String participant);
+
+ /**
+ * Called when an administrator revokes moderator privileges from a user. This means that the
+ * user will no longer be able to kick users, grant and revoke voice, invite other users,
+ * modify room's subject plus all the partcipants privileges.
+ *
+ * @param participant the participant that was revoked moderator privileges in the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void moderatorRevoked(String participant);
+
+ /**
+ * Called when an owner grants a user ownership on the room. This means that the user
+ * will be able to change defining room features as well as perform all administrative
+ * functions.
+ *
+ * @param participant the participant that was granted ownership on the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void ownershipGranted(String participant);
+
+ /**
+ * Called when an owner revokes a user ownership on the room. This means that the user
+ * will no longer be able to change defining room features as well as perform all
+ * administrative functions.
+ *
+ * @param participant the participant that was revoked ownership on the room
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void ownershipRevoked(String participant);
+
+ /**
+ * Called when an owner grants administrator privileges to a user. This means that the user
+ * will be able to perform administrative functions such as banning users and edit moderator
+ * list.
+ *
+ * @param participant the participant that was granted administrator privileges
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void adminGranted(String participant);
+
+ /**
+ * Called when an owner revokes administrator privileges from a user. This means that the user
+ * will no longer be able to perform administrative functions such as banning users and edit
+ * moderator list.
+ *
+ * @param participant the participant that was revoked administrator privileges
+ * (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void adminRevoked(String participant);
+
+ /**
+ * Called when a participant changed his/her nickname in the room. The new participant's
+ * nickname will be informed with the next available presence.
+ *
+ * @param participant the participant that was revoked administrator privileges
+ * (e.g. room@conference.jabber.org/nick).
+ * @param newNickname the new nickname that the participant decided to use.
+ */
+ public abstract void nicknameChanged(String participant, String newNickname);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/RoomInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/RoomInfo.java
new file mode 100644
index 000000000..694dd38d8
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/RoomInfo.java
@@ -0,0 +1,184 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.Form;
+
+/**
+ * Represents the room information that was discovered using Service Discovery. It's possible to
+ * obtain information about a room before joining the room but only for rooms that are public (i.e.
+ * rooms that may be discovered).
+ *
+ * @author Gaston Dombiak
+ */
+public class RoomInfo {
+
+ /**
+ * JID of the room. The node of the JID is commonly used as the ID of the room or name.
+ */
+ private String room;
+ /**
+ * Description of the room.
+ */
+ private String description = "";
+ /**
+ * Last known subject of the room.
+ */
+ private String subject = "";
+ /**
+ * Current number of occupants in the room.
+ */
+ private int occupantsCount = -1;
+ /**
+ * A room is considered members-only if an invitation is required in order to enter the room.
+ * Any user that is not a member of the room won't be able to join the room unless the user
+ * decides to register with the room (thus becoming a member).
+ */
+ private boolean membersOnly;
+ /**
+ * Moderated rooms enable only participants to speak. Users that join the room and aren't
+ * participants can't speak (they are just visitors).
+ */
+ private boolean moderated;
+ /**
+ * Every presence packet can include the JID of every occupant unless the owner deactives this
+ * configuration.
+ */
+ private boolean nonanonymous;
+ /**
+ * Indicates if users must supply a password to join the room.
+ */
+ private boolean passwordProtected;
+ /**
+ * Persistent rooms are saved to the database to make sure that rooms configurations can be
+ * restored in case the server goes down.
+ */
+ private boolean persistent;
+
+ RoomInfo(DiscoverInfo info) {
+ super();
+ this.room = info.getFrom();
+ // Get the information based on the discovered features
+ this.membersOnly = info.containsFeature("muc_membersonly");
+ this.moderated = info.containsFeature("muc_moderated");
+ this.nonanonymous = info.containsFeature("muc_nonanonymous");
+ this.passwordProtected = info.containsFeature("muc_passwordprotected");
+ this.persistent = info.containsFeature("muc_persistent");
+ // Get the information based on the discovered extended information
+ Form form = Form.getFormFrom(info);
+ if (form != null) {
+ this.description =
+ (String) form.getField("muc#roominfo_description").getValues().next();
+ this.subject = (String) form.getField("muc#roominfo_subject").getValues().next();
+ this.occupantsCount =
+ Integer.parseInt((String) form.getField("muc#roominfo_occupants").getValues()
+ .next());
+ }
+ }
+
+ /**
+ * Returns the JID of the room whose information was discovered.
+ *
+ * @return the JID of the room whose information was discovered.
+ */
+ public String getRoom() {
+ return room;
+ }
+
+ /**
+ * Returns the discovered description of the room.
+ *
+ * @return the discovered description of the room.
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Returns the discovered subject of the room. The subject may be empty if the room does not
+ * have a subject.
+ *
+ * @return the discovered subject of the room.
+ */
+ public String getSubject() {
+ return subject;
+ }
+
+ /**
+ * Returns the discovered number of occupants that are currently in the room. If this
+ * information was not discovered (i.e. the server didn't send it) then a value of -1 will be
+ * returned.
+ *
+ * @return the number of occupants that are currently in the room or -1 if that information was
+ * not provided by the server.
+ */
+ public int getOccupantsCount() {
+ return occupantsCount;
+ }
+
+ /**
+ * Returns true if the room has restricted the access so that only members may enter the room.
+ *
+ * @return true if the room has restricted the access so that only members may enter the room.
+ */
+ public boolean isMembersOnly() {
+ return membersOnly;
+ }
+
+ /**
+ * Returns true if the room enabled only participants to speak. Occupants with a role of
+ * visitor won't be able to speak in the room.
+ *
+ * @return true if the room enabled only participants to speak.
+ */
+ public boolean isModerated() {
+ return moderated;
+ }
+
+ /**
+ * Returns true if presence packets will include the JID of every occupant.
+ *
+ * @return true if presence packets will include the JID of every occupant.
+ */
+ public boolean isNonanonymous() {
+ return nonanonymous;
+ }
+
+ /**
+ * Returns true if users musy provide a valid password in order to join the room.
+ *
+ * @return true if users musy provide a valid password in order to join the room.
+ */
+ public boolean isPasswordProtected() {
+ return passwordProtected;
+ }
+
+ /**
+ * Returns true if the room will persist after the last occupant have left the room.
+ *
+ * @return true if the room will persist after the last occupant have left the room.
+ */
+ public boolean isPersistent() {
+ return persistent;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java
new file mode 100644
index 000000000..a966d7f19
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java
@@ -0,0 +1,38 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+/**
+ * A listener that is fired anytime a MUC room changes its subject.
+ *
+ * @author Gaston Dombiak
+ */
+public interface SubjectUpdatedListener {
+
+ /**
+ * Called when a MUC room has changed its subject.
+ *
+ * @param subject the new room's subject.
+ * @param from the user that changed the room's subject (e.g. room@conference.jabber.org/nick).
+ */
+ public abstract void subjectUpdated(String subject, String from);
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/UserStatusListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/UserStatusListener.java
new file mode 100644
index 000000000..6d243419a
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/UserStatusListener.java
@@ -0,0 +1,127 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.muc;
+
+/**
+ * A listener that is fired anytime your participant's status in a room is changed, such as the
+ * user being kicked, banned, or granted admin permissions.
+ *
+ * @author Gaston Dombiak
+ */
+public interface UserStatusListener {
+
+ /**
+ * Called when a moderator kicked your user from the room. This means that you are no longer
+ * participanting in the room.
+ *
+ * @param actor the moderator that kicked your user from the room (e.g. user@host.org).
+ * @param reason the reason provided by the actor to kick you from the room.
+ */
+ public abstract void kicked(String actor, String reason);
+
+ /**
+ * Called when a moderator grants voice to your user. This means that you were a visitor in
+ * the moderated room before and now you can participate in the room by sending messages to
+ * all occupants.
+ *
+ */
+ public abstract void voiceGranted();
+
+ /**
+ * Called when a moderator revokes voice from your user. This means that you were a
+ * participant in the room able to speak and now you are a visitor that can't send
+ * messages to the room occupants.
+ *
+ */
+ public abstract void voiceRevoked();
+
+ /**
+ * Called when an administrator or owner banned your user from the room. This means that you
+ * will no longer be able to join the room unless the ban has been removed.
+ *
+ * @param actor the administrator that banned your user (e.g. user@host.org).
+ * @param reason the reason provided by the administrator to banned you.
+ */
+ public abstract void banned(String actor, String reason);
+
+ /**
+ * Called when an administrator grants your user membership to the room. This means that you
+ * will be able to join the members-only room.
+ *
+ */
+ public abstract void membershipGranted();
+
+ /**
+ * Called when an administrator revokes your user membership to the room. This means that you
+ * will not be able to join the members-only room.
+ *
+ */
+ public abstract void membershipRevoked();
+
+ /**
+ * Called when an administrator grants moderator privileges to your user. This means that you
+ * will be able to kick users, grant and revoke voice, invite other users, modify room's
+ * subject plus all the partcipants privileges.
+ *
+ */
+ public abstract void moderatorGranted();
+
+ /**
+ * Called when an administrator revokes moderator privileges from your user. This means that
+ * you will no longer be able to kick users, grant and revoke voice, invite other users,
+ * modify room's subject plus all the partcipants privileges.
+ *
+ */
+ public abstract void moderatorRevoked();
+
+ /**
+ * Called when an owner grants to your user ownership on the room. This means that you
+ * will be able to change defining room features as well as perform all administrative
+ * functions.
+ *
+ */
+ public abstract void ownershipGranted();
+
+ /**
+ * Called when an owner revokes from your user ownership on the room. This means that you
+ * will no longer be able to change defining room features as well as perform all
+ * administrative functions.
+ *
+ */
+ public abstract void ownershipRevoked();
+
+ /**
+ * Called when an owner grants administrator privileges to your user. This means that you
+ * will be able to perform administrative functions such as banning users and edit moderator
+ * list.
+ *
+ */
+ public abstract void adminGranted();
+
+ /**
+ * Called when an owner revokes administrator privileges from your user. This means that you
+ * will no longer be able to perform administrative functions such as banning users and edit
+ * moderator list.
+ *
+ */
+ public abstract void adminRevoked();
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/package.html
new file mode 100644
index 000000000..dcfaeaace
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/package.html
@@ -0,0 +1 @@
+<body>Classes and Interfaces that implement Multi-User Chat (MUC).</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/package.html
new file mode 100644
index 000000000..d574a2a4d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/package.html
@@ -0,0 +1 @@
+<body>Smack extensions API.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Bytestream.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Bytestream.java
new file mode 100644
index 000000000..57136218d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Bytestream.java
@@ -0,0 +1,481 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+
+import java.util.*;
+
+/**
+ * A packet representing part of a Socks5 Bytestream negotiation.
+ *
+ * @author Alexander Wenckus
+ */
+public class Bytestream extends IQ {
+
+ private String sessionID;
+
+ private Mode mode = Mode.TCP;
+
+ private final List streamHosts = new ArrayList();
+
+ private StreamHostUsed usedHost;
+
+ private Activate toActivate;
+
+ /**
+ * The default constructor
+ */
+ public Bytestream() {
+ super();
+ }
+
+ /**
+ * A constructor where the session ID can be specified.
+ *
+ * @param SID The session ID related to the negotiation.
+ * @see #setSessionID(String)
+ */
+ public Bytestream(final String SID) {
+ super();
+ setSessionID(SID);
+ }
+
+ /**
+ * Set the session ID related to the Byte Stream. The session ID is a unique
+ * identifier used to differentiate between stream negotations.
+ *
+ * @param sessionID
+ */
+ public void setSessionID(final String sessionID) {
+ this.sessionID = sessionID;
+ }
+
+ /**
+ * Returns the session ID related to the Byte Stream negotiation.
+ *
+ * @return Returns the session ID related to the Byte Stream negotiation.
+ * @see #setSessionID(String)
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Set the transport mode. This should be put in the initiation of the
+ * interaction.
+ *
+ * @param mode
+ * @see Mode
+ */
+ public void setMode(final Mode mode) {
+ this.mode = mode;
+ }
+
+ /**
+ * Returns the transport mode.
+ *
+ * @return Returns the transport mode.
+ * @see #setMode(Mode)
+ */
+ public Mode getMode() {
+ return mode;
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can connect to to
+ * receive the file.
+ *
+ * @param JID The jabber ID of the stream host.
+ * @param address The internet address of the stream host.
+ * @return The added stream host.
+ */
+ public StreamHost addStreamHost(final String JID, final String address) {
+ return addStreamHost(JID, address, 0);
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can connect to to
+ * receive the file.
+ *
+ * @param JID The jabber ID of the stream host.
+ * @param address The internet address of the stream host.
+ * @param port The port on which the remote host is seeking connections.
+ * @return The added stream host.
+ */
+ public StreamHost addStreamHost(final String JID, final String address,
+ final int port) {
+ StreamHost host = new StreamHost(JID, address);
+ host.setPort(port);
+ addStreamHost(host);
+
+ return host;
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can transfer the file
+ * through.
+ *
+ * @param host The potential stream host.
+ */
+ public void addStreamHost(final StreamHost host) {
+ streamHosts.add(host);
+ }
+
+ /**
+ * Returns the list of stream hosts contained in the packet.
+ *
+ * @return Returns the list of stream hosts contained in the packet.
+ */
+ public Collection getStreamHosts() {
+ return Collections.unmodifiableCollection(streamHosts);
+ }
+
+ /**
+ * Returns the stream host related to the given jabber ID, or null if there
+ * is none.
+ *
+ * @param JID The jabber ID of the desired stream host.
+ * @return Returns the stream host related to the given jabber ID, or null
+ * if there is none.
+ */
+ public StreamHost getStreamHost(final String JID) {
+ StreamHost host;
+ for (Iterator it = streamHosts.iterator(); it.hasNext();) {
+ host = (StreamHost) it.next();
+ if (host.getJID().equals(JID)) {
+ return host;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the count of stream hosts contained in this packet.
+ *
+ * @return Returns the count of stream hosts contained in this packet.
+ */
+ public int countStreamHosts() {
+ return streamHosts.size();
+ }
+
+ /**
+ * Upon connecting to the stream host the target of the stream replys to the
+ * initiator with the jabber id of the Socks5 host that they used.
+ *
+ * @param JID The jabber ID of the used host.
+ */
+ public void setUsedHost(final String JID) {
+ this.usedHost = new StreamHostUsed(JID);
+ }
+
+ /**
+ * Returns the Socks5 host connected to by the remote user.
+ *
+ * @return Returns the Socks5 host connected to by the remote user.
+ */
+ public StreamHostUsed getUsedHost() {
+ return usedHost;
+ }
+
+ /**
+ * Returns the activate element of the packet sent to the proxy host to
+ * verify the identity of the initiator and match them to the appropriate
+ * stream.
+ *
+ * @return Returns the activate element of the packet sent to the proxy host
+ * to verify the identity of the initiator and match them to the
+ * appropriate stream.
+ */
+ public Activate getToActivate() {
+ return toActivate;
+ }
+
+ /**
+ * Upon the response from the target of the used host the activate packet is
+ * sent to the Socks5 proxy. The proxy will activate the stream or return an
+ * error after verifying the identity of the initiator, using the activate
+ * packet.
+ *
+ * @param targetID The jabber ID of the target of the file transfer.
+ */
+ public void setToActivate(final String targetID) {
+ this.toActivate = new Activate(targetID);
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+
+ buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\"");
+ if (this.getType().equals(IQ.Type.SET)) {
+ if (getSessionID() != null)
+ buf.append(" sid=\"").append(getSessionID()).append("\"");
+ if (getMode() != null)
+ buf.append(" mode = \"").append(getMode()).append("\"");
+ buf.append(">");
+ if (getToActivate() == null) {
+ for (Iterator it = getStreamHosts().iterator(); it.hasNext();)
+ buf.append(((StreamHost) it.next()).toXML());
+ }
+ else {
+ buf.append(getToActivate().toXML());
+ }
+ }
+ else if (this.getType().equals(IQ.Type.RESULT)) {
+ buf.append(">");
+ if (getUsedHost() != null)
+ buf.append(getUsedHost().toXML());
+ }
+ else {
+ return null;
+ }
+ buf.append("</query>");
+
+ return buf.toString();
+ }
+
+ /**
+ * Packet extension that represents a potential Socks5 proxy for the file
+ * transfer. Stream hosts are forwared to the target of the file transfer
+ * who then chooses and connects to one.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class StreamHost implements PacketExtension {
+
+ public static String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "streamhost";
+
+ private final String JID;
+
+ private final String addy;
+
+ private int port = 0;
+
+ /**
+ * Default constructor.
+ *
+ * @param JID The jabber ID of the stream host.
+ * @param address The internet address of the stream host.
+ */
+ public StreamHost(final String JID, final String address) {
+ this.JID = JID;
+ this.addy = address;
+ }
+
+ /**
+ * Returns the jabber ID of the stream host.
+ *
+ * @return Returns the jabber ID of the stream host.
+ */
+ public String getJID() {
+ return JID;
+ }
+
+ /**
+ * Returns the internet address of the stream host.
+ *
+ * @return Returns the internet address of the stream host.
+ */
+ public String getAddress() {
+ return addy;
+ }
+
+ /**
+ * Sets the port of the stream host.
+ *
+ * @param port The port on which the potential stream host would accept
+ * the connection.
+ */
+ public void setPort(final int port) {
+ this.port = port;
+ }
+
+ /**
+ * Returns the port on which the potential stream host would accept the
+ * connection.
+ *
+ * @return Returns the port on which the potential stream host would
+ * accept the connection.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+
+ buf.append("<").append(getElementName()).append(" ");
+ buf.append("jid=\"").append(getJID()).append("\" ");
+ buf.append("host=\"").append(getAddress()).append("\" ");
+ if (getPort() != 0)
+ buf.append("port=\"").append(getPort()).append("\"");
+ else
+ buf.append("zeroconf=\"_jabber.bytestreams\"");
+ buf.append("/>");
+
+ return buf.toString();
+ }
+ }
+
+ /**
+ * After selected a Socks5 stream host and successfully connecting, the
+ * target of the file transfer returns a byte stream packet with the stream
+ * host used extension.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class StreamHostUsed implements PacketExtension {
+
+ public String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "streamhost-used";
+
+ private final String JID;
+
+ /**
+ * Default constructor.
+ *
+ * @param JID The jabber ID of the selected stream host.
+ */
+ public StreamHostUsed(final String JID) {
+ this.JID = JID;
+ }
+
+ /**
+ * Returns the jabber ID of the selected stream host.
+ *
+ * @return Returns the jabber ID of the selected stream host.
+ */
+ public String getJID() {
+ return JID;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" ");
+ buf.append("jid=\"").append(getJID()).append("\" ");
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * The packet sent by the stream initiator to the stream proxy to activate
+ * the connection.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class Activate implements PacketExtension {
+
+ public String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "activate";
+
+ private final String target;
+
+ /**
+ * Default constructor specifying the target of the stream.
+ *
+ * @param target The target of the stream.
+ */
+ public Activate(final String target) {
+ this.target = target;
+ }
+
+ /**
+ * Returns the target of the activation.
+ *
+ * @return Returns the target of the activation.
+ */
+ public String getTarget() {
+ return target;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(">");
+ buf.append(getTarget());
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * The stream can be either a TCP stream or a UDP stream.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class Mode {
+
+ /**
+ * A TCP based stream.
+ */
+ public static Mode TCP = new Mode("tcp");
+
+ /**
+ * A UDP based stream.
+ */
+ public static Mode UDP = new Mode("udp");
+
+ private final String modeString;
+
+ private Mode(final String mode) {
+ this.modeString = mode;
+ }
+
+ public String toString() {
+ return modeString;
+ }
+
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Mode))
+ return false;
+ return modeString.equals(((Mode) obj).modeString);
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DataForm.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DataForm.java
new file mode 100644
index 000000000..82b2939cc
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DataForm.java
@@ -0,0 +1,296 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.FormField;
+
+/**
+ * Represents a form that could be use for gathering data as well as for reporting data
+ * returned from a search.
+ *
+ * @author Gaston Dombiak
+ */
+public class DataForm implements PacketExtension {
+
+ private String type;
+ private String title;
+ private List instructions = new ArrayList();
+ private ReportedData reportedData;
+ private List items = new ArrayList();
+ private List fields = new ArrayList();
+
+ public DataForm(String type) {
+ this.type = type;
+ }
+
+ /**
+ * Returns the meaning of the data within the context. The data could be part of a form
+ * to fill out, a form submission or data results.<p>
+ *
+ * Possible form types are:
+ * <ul>
+ * <li>form -> This packet contains a form to fill out. Display it to the user (if your
+ * program can).</li>
+ * <li>submit -> The form is filled out, and this is the data that is being returned from
+ * the form.</li>
+ * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li>
+ * <li>result -> Data results being returned from a search, or some other query.</li>
+ * </ul>
+ *
+ * @return the form's type.
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Returns the description of the data. It is similar to the title on a web page or an X
+ * window. You can put a <title/> on either a form to fill out, or a set of data results.
+ *
+ * @return description of the data.
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Returns an Iterator for the list of instructions that explain how to fill out the form and
+ * what the form is about. The dataform could include multiple instructions since each
+ * instruction could not contain newlines characters. Join the instructions together in order
+ * to show them to the user.
+ *
+ * @return an Iterator for the list of instructions that explain how to fill out the form.
+ */
+ public Iterator getInstructions() {
+ synchronized (instructions) {
+ return Collections.unmodifiableList(new ArrayList(instructions)).iterator();
+ }
+ }
+
+ /**
+ * Returns the fields that will be returned from a search.
+ *
+ * @return fields that will be returned from a search.
+ */
+ public ReportedData getReportedData() {
+ return reportedData;
+ }
+
+ /**
+ * Returns an Iterator for the items returned from a search.
+ *
+ * @return an Iterator for the items returned from a search.
+ */
+ public Iterator getItems() {
+ synchronized (items) {
+ return Collections.unmodifiableList(new ArrayList(items)).iterator();
+ }
+ }
+
+ /**
+ * Returns an Iterator for the fields that are part of the form.
+ *
+ * @return an Iterator for the fields that are part of the form.
+ */
+ public Iterator getFields() {
+ synchronized (fields) {
+ return Collections.unmodifiableList(new ArrayList(fields)).iterator();
+ }
+ }
+
+ public String getElementName() {
+ return "x";
+ }
+
+ public String getNamespace() {
+ return "jabber:x:data";
+ }
+
+ /**
+ * Sets the description of the data. It is similar to the title on a web page or an X window.
+ * You can put a <title/> on either a form to fill out, or a set of data results.
+ *
+ * @param title description of the data.
+ */
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ /**
+ * Sets the list of instructions that explain how to fill out the form and what the form is
+ * about. The dataform could include multiple instructions since each instruction could not
+ * contain newlines characters.
+ *
+ * @param instructions list of instructions that explain how to fill out the form.
+ */
+ public void setInstructions(List instructions) {
+ this.instructions = instructions;
+ }
+
+ /**
+ * Sets the fields that will be returned from a search.
+ *
+ * @param reportedData the fields that will be returned from a search.
+ */
+ public void setReportedData(ReportedData reportedData) {
+ this.reportedData = reportedData;
+ }
+
+ /**
+ * Adds a new field as part of the form.
+ *
+ * @param field the field to add to the form.
+ */
+ public void addField(FormField field) {
+ synchronized (fields) {
+ fields.add(field);
+ }
+ }
+
+ /**
+ * Adds a new instruction to the list of instructions that explain how to fill out the form
+ * and what the form is about. The dataform could include multiple instructions since each
+ * instruction could not contain newlines characters.
+ *
+ * @param instruction the new instruction that explain how to fill out the form.
+ */
+ public void addInstruction(String instruction) {
+ synchronized (instructions) {
+ instructions.add(instruction);
+ }
+ }
+
+ /**
+ * Adds a new item returned from a search.
+ *
+ * @param item the item returned from a search.
+ */
+ public void addItem(Item item) {
+ synchronized (items) {
+ items.add(item);
+ }
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+ "\" type=\"" + getType() +"\">");
+ if (getTitle() != null) {
+ buf.append("<title>").append(getTitle()).append("</title>");
+ }
+ for (Iterator it=getInstructions(); it.hasNext();) {
+ buf.append("<instructions>").append(it.next()).append("</instructions>");
+ }
+ // Append the list of fields returned from a search
+ if (getReportedData() != null) {
+ buf.append(getReportedData().toXML());
+ }
+ // Loop through all the items returned from a search and append them to the string buffer
+ for (Iterator i = getItems(); i.hasNext();) {
+ Item item = (Item) i.next();
+ buf.append(item.toXML());
+ }
+ // Loop through all the form fields and append them to the string buffer
+ for (Iterator i = getFields(); i.hasNext();) {
+ FormField field = (FormField) i.next();
+ buf.append(field.toXML());
+ }
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ /**
+ *
+ * Represents the fields that will be returned from a search. This information is useful when
+ * you try to use the jabber:iq:search namespace to return dynamic form information.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class ReportedData {
+ private List fields = new ArrayList();
+
+ public ReportedData(List fields) {
+ this.fields = fields;
+ }
+
+ /**
+ * Returns the fields returned from a search.
+ *
+ * @return the fields returned from a search.
+ */
+ public Iterator getFields() {
+ return Collections.unmodifiableList(new ArrayList(fields)).iterator();
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<reported>");
+ // Loop through all the form items and append them to the string buffer
+ for (Iterator i = getFields(); i.hasNext();) {
+ FormField field = (FormField) i.next();
+ buf.append(field.toXML());
+ }
+ buf.append("</reported>");
+ return buf.toString();
+ }
+ }
+
+ /**
+ *
+ * Represents items of reported data.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Item {
+ private List fields = new ArrayList();
+
+ public Item(List fields) {
+ this.fields = fields;
+ }
+
+ /**
+ * Returns the fields that define the data that goes with the item.
+ *
+ * @return the fields that define the data that goes with the item.
+ */
+ public Iterator getFields() {
+ return Collections.unmodifiableList(new ArrayList(fields)).iterator();
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<item>");
+ // Loop through all the form items and append them to the string buffer
+ for (Iterator i = getFields(); i.hasNext();) {
+ FormField field = (FormField) i.next();
+ buf.append(field.toXML());
+ }
+ buf.append("</item>");
+ return buf.toString();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java
new file mode 100644
index 000000000..c000d2eed
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java
@@ -0,0 +1,137 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.util.Map;
+import java.util.Iterator;
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * Default implementation of the PrivateData interface. Unless a PrivateDataProvider
+ * is registered with the PrivateDataManager class, instances of this class will be
+ * returned when getting private data.<p>
+ *
+ * This class provides a very simple representation of an XML sub-document. Each element
+ * is a key in a Map with its CDATA being the value. For example, given the following
+ * XML sub-document:
+ *
+ * <pre>
+ * &lt;foo xmlns="http://bar.com"&gt;
+ * &lt;color&gt;blue&lt;/color&gt;
+ * &lt;food&gt;pizza&lt;/food&gt;
+ * &lt;/foo&gt;</pre>
+ *
+ * In this case, getValue("color") would return "blue", and getValue("food") would
+ * return "pizza". This parsing mechanism mechanism is very simplistic and will not work
+ * as desired in all cases (for example, if some of the elements have attributes. In those
+ * cases, a custom {@link org.jivesoftware.smackx.provider.PrivateDataProvider} should be used.
+ *
+ * @author Matt Tucker
+ */
+public class DefaultPrivateData implements PrivateData {
+
+ private String elementName;
+ private String namespace;
+ private Map map;
+
+ /**
+ * Creates a new generic private data object.
+ *
+ * @param elementName the name of the element of the XML sub-document.
+ * @param namespace the namespace of the element.
+ */
+ public DefaultPrivateData(String elementName, String namespace) {
+ this.elementName = elementName;
+ this.namespace = namespace;
+ }
+
+ /**
+ * Returns the XML element name of the private data sub-packet root element.
+ *
+ * @return the XML element name of the packet extension.
+ */
+ public String getElementName() {
+ return elementName;
+ }
+
+ /**
+ * Returns the XML namespace of the private data sub-packet root element.
+ *
+ * @return the XML namespace of the packet extension.
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\">");
+ for (Iterator i=getNames(); i.hasNext(); ) {
+ String name = (String)i.next();
+ String value = getValue(name);
+ buf.append("<").append(name).append(">");
+ buf.append(value);
+ buf.append("</").append(name).append(">");
+ }
+ buf.append("</").append(elementName).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * Returns an Iterator for the names that can be used to get
+ * values of the private data.
+ *
+ * @return an Iterator for the names.
+ */
+ public synchronized Iterator getNames() {
+ if (map == null) {
+ return Collections.EMPTY_LIST.iterator();
+ }
+ return Collections.unmodifiableMap(new HashMap(map)).keySet().iterator();
+ }
+
+ /**
+ * Returns a value given a name.
+ *
+ * @param name the name.
+ * @return the value.
+ */
+ public synchronized String getValue(String name) {
+ if (map == null) {
+ return null;
+ }
+ return (String)map.get(name);
+ }
+
+ /**
+ * Sets a value given the name.
+ *
+ * @param name the name.
+ * @param value the value.
+ */
+ public synchronized void setValue(String name, String value) {
+ if (map == null) {
+ map = new HashMap();
+ }
+ map.put(name, value);
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DelayInformation.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DelayInformation.java
new file mode 100644
index 000000000..c04409b3a
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DelayInformation.java
@@ -0,0 +1,146 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents timestamp information about data stored for later delivery. A DelayInformation will
+ * always includes the timestamp when the packet was originally sent and may include more
+ * information such as the JID of the entity that originally sent the packet as well as the reason
+ * for the dealy.<p>
+ *
+ * For more information see <a href="http://www.jabber.org/jeps/jep-0091.html">JEP-91</a>.
+ *
+ * @author Gaston Dombiak
+ */
+public class DelayInformation implements PacketExtension {
+
+ public static SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+ /**
+ * New date format based on JEP-82 that some clients may use when sending delayed dates.
+ * JEP-91 is using a SHOULD other servers or clients may be using this format instead of the
+ * old UTC format.
+ */
+ public static SimpleDateFormat NEW_UTC_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+
+ static {
+ UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));
+ NEW_UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ private Date stamp;
+ private String from;
+ private String reason;
+
+ /**
+ * Creates a new instance with the specified timestamp.
+ */
+ public DelayInformation(Date stamp) {
+ super();
+ this.stamp = stamp;
+ }
+
+ /**
+ * Returns the JID of the entity that originally sent the packet or that delayed the
+ * delivery of the packet or <tt>null</tt> if this information is not available.
+ *
+ * @return the JID of the entity that originally sent the packet or that delayed the
+ * delivery of the packet.
+ */
+ public String getFrom() {
+ return from;
+ }
+
+ /**
+ * Sets the JID of the entity that originally sent the packet or that delayed the
+ * delivery of the packet or <tt>null</tt> if this information is not available.
+ *
+ * @param from the JID of the entity that originally sent the packet.
+ */
+ public void setFrom(String from) {
+ this.from = from;
+ }
+
+ /**
+ * Returns the timstamp when the packet was originally sent. The returned Date is
+ * be understood as UTC.
+ *
+ * @return the timstamp when the packet was originally sent.
+ */
+ public Date getStamp() {
+ return stamp;
+ }
+
+ /**
+ * Returns a natural-language description of the reason for the delay or <tt>null</tt> if
+ * this information is not available.
+ *
+ * @return a natural-language description of the reason for the delay or <tt>null</tt>.
+ */
+ public String getReason() {
+ return reason;
+ }
+
+ /**
+ * Sets a natural-language description of the reason for the delay or <tt>null</tt> if
+ * this information is not available.
+ *
+ * @param reason a natural-language description of the reason for the delay or <tt>null</tt>.
+ */
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ public String getElementName() {
+ return "x";
+ }
+
+ public String getNamespace() {
+ return "jabber:x:delay";
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+ "\"");
+ buf.append(" stamp=\"");
+ synchronized (UTC_FORMAT) {
+ buf.append(UTC_FORMAT.format(stamp));
+ }
+ buf.append("\"");
+ if (from != null && from.length() > 0) {
+ buf.append(" from=\"").append(from).append("\"");
+ }
+ buf.append(">");
+ if (reason != null && reason.length() > 0) {
+ buf.append(reason);
+ }
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverInfo.java
new file mode 100644
index 000000000..0694a0ceb
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverInfo.java
@@ -0,0 +1,268 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.util.*;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * A DiscoverInfo IQ packet, which is used by XMPP clients to request and receive information
+ * to/from other XMPP entities.<p>
+ *
+ * The received information may contain one or more identities of the requested XMPP entity, and
+ * a list of supported features by the requested XMPP entity.
+ *
+ * @author Gaston Dombiak
+ */
+public class DiscoverInfo extends IQ {
+
+ private List features = new ArrayList();
+ private List identities = new ArrayList();
+ private String node;
+
+ /**
+ * Adds a new feature to the discovered information.
+ *
+ * @param feature the discovered feature
+ */
+ public void addFeature(String feature) {
+ addFeature(new DiscoverInfo.Feature(feature));
+ }
+
+ private void addFeature(Feature feature) {
+ synchronized (features) {
+ features.add(feature);
+ }
+ }
+
+ /**
+ * Returns the discovered features of an XMPP entity.
+ *
+ * @return an Iterator on the discovered features of an XMPP entity
+ */
+ Iterator getFeatures() {
+ synchronized (features) {
+ return Collections.unmodifiableList(new ArrayList(features)).iterator();
+ }
+ }
+
+ /**
+ * Adds a new identity of the requested entity to the discovered information.
+ *
+ * @param identity the discovered entity's identity
+ */
+ public void addIdentity(Identity identity) {
+ synchronized (identities) {
+ identities.add(identity);
+ }
+ }
+
+ /**
+ * Returns the discovered identities of an XMPP entity.
+ *
+ * @return an Iterator on the discoveted identities
+ */
+ public Iterator getIdentities() {
+ synchronized (identities) {
+ return Collections.unmodifiableList(new ArrayList(identities)).iterator();
+ }
+ }
+
+ /**
+ * Returns the node attribute that supplements the 'jid' attribute. A node is merely
+ * something that is associated with a JID and for which the JID can provide information.<p>
+ *
+ * Node attributes SHOULD be used only when trying to provide or query information which
+ * is not directly addressable.
+ *
+ * @return the node attribute that supplements the 'jid' attribute
+ */
+ public String getNode() {
+ return node;
+ }
+
+ /**
+ * Sets the node attribute that supplements the 'jid' attribute. A node is merely
+ * something that is associated with a JID and for which the JID can provide information.<p>
+ *
+ * Node attributes SHOULD be used only when trying to provide or query information which
+ * is not directly addressable.
+ *
+ * @param node the node attribute that supplements the 'jid' attribute
+ */
+ public void setNode(String node) {
+ this.node = node;
+ }
+
+ /**
+ * Returns true if the specified feature is part of the discovered information.
+ *
+ * @param feature the feature to check
+ * @return true if the requestes feature has been discovered
+ */
+ public boolean containsFeature(String feature) {
+ for (Iterator it = getFeatures(); it.hasNext();) {
+ if (feature.equals(((DiscoverInfo.Feature) it.next()).getVar()))
+ return true;
+ }
+ return false;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"http://jabber.org/protocol/disco#info\"");
+ if (getNode() != null) {
+ buf.append(" node=\"");
+ buf.append(getNode());
+ buf.append("\"");
+ }
+ buf.append(">");
+ synchronized (identities) {
+ for (int i = 0; i < identities.size(); i++) {
+ Identity identity = (Identity) identities.get(i);
+ buf.append(identity.toXML());
+ }
+ }
+ synchronized (features) {
+ for (int i = 0; i < features.size(); i++) {
+ Feature feature = (Feature) features.get(i);
+ buf.append(feature.toXML());
+ }
+ }
+ // Add packet extensions, if any are defined.
+ buf.append(getExtensionsXML());
+ buf.append("</query>");
+ return buf.toString();
+ }
+
+ /**
+ * Represents the identity of a given XMPP entity. An entity may have many identities but all
+ * the identities SHOULD have the same name.<p>
+ *
+ * Refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a>
+ * in order to get the official registry of values for the <i>category</i> and <i>type</i>
+ * attributes.
+ *
+ */
+ public static class Identity {
+
+ private String category;
+ private String name;
+ private String type;
+
+ /**
+ * Creates a new identity for an XMPP entity.
+ *
+ * @param category the entity's category.
+ * @param name the entity's name.
+ */
+ public Identity(String category, String name) {
+ this.category = category;
+ this.name = name;
+ }
+
+ /**
+ * 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>
+ *
+ * @return the entity's category.
+ */
+ public String getCategory() {
+ return category;
+ }
+
+ /**
+ * Returns the identity's name.
+ *
+ * @return the identity's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * 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>
+ *
+ * @return the entity's type.
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Sets 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>
+ *
+ * @param type the identity's type.
+ */
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<identity category=\"").append(category).append("\"");
+ buf.append(" name=\"").append(name).append("\"");
+ if (type != null) {
+ buf.append(" type=\"").append(type).append("\"");
+ }
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Represents the features offered by the item. This information helps requestors determine
+ * what actions are possible with regard to this item (registration, search, join, etc.)
+ * as well as specific feature types of interest, if any (e.g., for the purpose of feature
+ * negotiation).
+ */
+ public static class Feature {
+
+ private String variable;
+
+ /**
+ * Creates a new feature offered by an XMPP entity or item.
+ *
+ * @param variable the feature's variable.
+ */
+ public Feature(String variable) {
+ this.variable = variable;
+ }
+
+ /**
+ * Returns the feature's variable.
+ *
+ * @return the feature's variable.
+ */
+ public String getVar() {
+ return variable;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<feature var=\"").append(variable).append("\"/>");
+ return buf.toString();
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverItems.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverItems.java
new file mode 100644
index 000000000..0c264aea9
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverItems.java
@@ -0,0 +1,235 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.util.*;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * A DiscoverItems IQ packet, which is used by XMPP clients to request and receive items
+ * associated with XMPP entities.<p>
+ *
+ * The items could also be queried in order to discover if they contain items inside. Some items
+ * may be addressable by its JID and others may require to be addressed by a JID and a node name.
+ *
+ * @author Gaston Dombiak
+ */
+public class DiscoverItems extends IQ {
+
+ private List items = new ArrayList();
+ private String node;
+
+ /**
+ * Adds a new item to the discovered information.
+ *
+ * @param item the discovered entity's item
+ */
+ public void addItem(Item item) {
+ synchronized (items) {
+ items.add(item);
+ }
+ }
+
+ /**
+ * Returns the discovered items of the queried XMPP entity.
+ *
+ * @return an Iterator on the discovered entity's items
+ */
+ public Iterator getItems() {
+ synchronized (items) {
+ return Collections.unmodifiableList(new ArrayList(items)).iterator();
+ }
+ }
+
+ /**
+ * Returns the node attribute that supplements the 'jid' attribute. A node is merely
+ * something that is associated with a JID and for which the JID can provide information.<p>
+ *
+ * Node attributes SHOULD be used only when trying to provide or query information which
+ * is not directly addressable.
+ *
+ * @return the node attribute that supplements the 'jid' attribute
+ */
+ public String getNode() {
+ return node;
+ }
+
+ /**
+ * Sets the node attribute that supplements the 'jid' attribute. A node is merely
+ * something that is associated with a JID and for which the JID can provide information.<p>
+ *
+ * Node attributes SHOULD be used only when trying to provide or query information which
+ * is not directly addressable.
+ *
+ * @param node the node attribute that supplements the 'jid' attribute
+ */
+ public void setNode(String node) {
+ this.node = node;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"http://jabber.org/protocol/disco#items\"");
+ if (getNode() != null) {
+ buf.append(" node=\"");
+ buf.append(getNode());
+ buf.append("\"");
+ }
+ buf.append(">");
+ synchronized (items) {
+ for (int i = 0; i < items.size(); i++) {
+ Item item = (Item) items.get(i);
+ buf.append(item.toXML());
+ }
+ }
+ buf.append("</query>");
+ return buf.toString();
+ }
+
+ /**
+ * An item is associated with an XMPP Entity, usually thought of a children of the parent
+ * entity and normally are addressable as a JID.<p>
+ *
+ * An item associated with an entity may not be addressable as a JID. In order to handle
+ * such items, Service Discovery uses an optional 'node' attribute that supplements the
+ * 'jid' attribute.
+ */
+ public static class Item {
+
+ /**
+ * Request to create or update the item.
+ */
+ public static final String UPDATE_ACTION = "update";
+
+ /**
+ * Request to remove the item.
+ */
+ public static final String REMOVE_ACTION = "remove";
+
+ private String entityID;
+ private String name;
+ private String node;
+ private String action;
+
+ /**
+ * Create a new Item associated with a given entity.
+ *
+ * @param entityID the id of the entity that contains the item
+ */
+ public Item(String entityID) {
+ this.entityID = entityID;
+ }
+
+ /**
+ * Returns the entity's ID.
+ *
+ * @return the entity's ID.
+ */
+ public String getEntityID() {
+ return entityID;
+ }
+
+ /**
+ * Returns the entity's name.
+ *
+ * @return the entity's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the entity's name.
+ *
+ * @param name the entity's name.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the node attribute that supplements the 'jid' attribute. A node is merely
+ * something that is associated with a JID and for which the JID can provide information.<p>
+ *
+ * Node attributes SHOULD be used only when trying to provide or query information which
+ * is not directly addressable.
+ *
+ * @return the node attribute that supplements the 'jid' attribute
+ */
+ public String getNode() {
+ return node;
+ }
+
+ /**
+ * Sets the node attribute that supplements the 'jid' attribute. A node is merely
+ * something that is associated with a JID and for which the JID can provide information.<p>
+ *
+ * Node attributes SHOULD be used only when trying to provide or query information which
+ * is not directly addressable.
+ *
+ * @param node the node attribute that supplements the 'jid' attribute
+ */
+ public void setNode(String node) {
+ this.node = node;
+ }
+
+ /**
+ * Returns the action that specifies the action being taken for this item. Possible action
+ * values are: "update" and "remove". Update should either create a new entry if the node
+ * and jid combination does not already exist, or simply update an existing entry. If
+ * "remove" is used as the action, the item should be removed from persistent storage.
+ *
+ * @return the action being taken for this item
+ */
+ public String getAction() {
+ return action;
+ }
+
+ /**
+ * Sets the action that specifies the action being taken for this item. Possible action
+ * values are: "update" and "remove". Update should either create a new entry if the node
+ * and jid combination does not already exist, or simply update an existing entry. If
+ * "remove" is used as the action, the item should be removed from persistent storage.
+ *
+ * @param action the action being taken for this item
+ */
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<item jid=\"").append(entityID).append("\"");
+ if (name != null) {
+ buf.append(" name=\"").append(name).append("\"");
+ }
+ if (node != null) {
+ buf.append(" node=\"").append(node).append("\"");
+ }
+ if (action != null) {
+ buf.append(" action=\"").append(action).append("\"");
+ }
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/IBBExtensions.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/IBBExtensions.java
new file mode 100644
index 000000000..48c1cb150
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/IBBExtensions.java
@@ -0,0 +1,241 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * The different extensions used throughtout the negotiation and transfer
+ * process.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class IBBExtensions {
+
+ public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
+
+ private abstract static class IBB extends IQ {
+ final String sid;
+
+ private IBB(final String sid) {
+ this.sid = sid;
+ }
+
+ /**
+ * Returns the unique stream ID for this file transfer.
+ *
+ * @return Returns the unique stream ID for this file transfer.
+ */
+ public String getSessionID() {
+ return sid;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+ }
+
+ /**
+ * Represents a request to open the file transfer.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public static class Open extends IBB {
+
+ public static final String ELEMENT_NAME = "open";
+
+ private final int blockSize;
+
+ /**
+ * Constructs an open packet.
+ *
+ * @param sid
+ * The streamID of the file transfer.
+ * @param blockSize
+ * The block size of the file transfer.
+ */
+ public Open(final String sid, final int blockSize) {
+ super(sid);
+ this.blockSize = blockSize;
+ }
+
+ /**
+ * The size blocks in which the data will be sent.
+ *
+ * @return The size blocks in which the data will be sent.
+ */
+ public int getBlockSize() {
+ return blockSize;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" ");
+ buf.append("sid=\"").append(getSessionID()).append("\" ");
+ buf.append("block-size=\"").append(getBlockSize()).append("\"");
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * A data packet containing a portion of the file being sent encoded in
+ * base64.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public static class Data implements PacketExtension {
+
+ private long seq;
+
+ private String data;
+
+ public static final String ELEMENT_NAME = "data";
+
+ final String sid;
+
+ /**
+ * Returns the unique stream ID identifying this file transfer.
+ *
+ * @return Returns the unique stream ID identifying this file transfer.
+ */
+ public String getSessionID() {
+ return sid;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ /**
+ * A constructor.
+ *
+ * @param sid
+ * The stream ID.
+ */
+ public Data(final String sid) {
+ this.sid = sid;
+ }
+
+ public Data(final String sid, final long seq, final String data) {
+ this(sid);
+ this.seq = seq;
+ this.data = data;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ /**
+ * Returns the data contained in this packet.
+ *
+ * @return Returns the data contained in this packet.
+ */
+ public String getData() {
+ return data;
+ }
+
+ /**
+ * Sets the data contained in this packet.
+ *
+ * @param data
+ * The data encoded in base65
+ */
+ public void setData(final String data) {
+ this.data = data;
+ }
+
+ /**
+ * Returns the sequence of this packet in regard to the other data
+ * packets.
+ *
+ * @return Returns the sequence of this packet in regard to the other
+ * data packets.
+ */
+ public long getSeq() {
+ return seq;
+ }
+
+ /**
+ * Sets the sequence of this packet.
+ *
+ * @param seq
+ * A number between 0 and 65535
+ */
+ public void setSeq(final long seq) {
+ this.seq = seq;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace())
+ .append("\" ");
+ buf.append("sid=\"").append(getSessionID()).append("\" ");
+ buf.append("seq=\"").append(getSeq()).append("\"");
+ buf.append(">");
+ buf.append(getData());
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Represents the closing of the file transfer.
+ *
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public static class Close extends IBB {
+ public static final String ELEMENT_NAME = "close";
+
+ /**
+ * The constructor.
+ *
+ * @param sid
+ * The unique stream ID identifying this file transfer.
+ */
+ public Close(String sid) {
+ super(sid);
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" ");
+ buf.append("sid=\"").append(getSessionID()).append("\"");
+ buf.append("/>");
+ return buf.toString();
+ }
+
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/LastActivity.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/LastActivity.java
new file mode 100644
index 000000000..7145784e8
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/LastActivity.java
@@ -0,0 +1,158 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * A last activity IQ for retrieving information about the last activity associated with a Jabber ID.
+ * LastActivity (JEP-012) allows for retrieval of how long a particular user has been idle and the
+ * message the specified when doing so.
+ * To get the last activity of a user, simple send the LastActivity packet to them, as in the
+ * following code example:
+ * <p/>
+ * <pre>
+ * XMPPConnection con = new XMPPConnection("jabber.org");
+ * con.login("john", "doe");
+ * LastActivity activity = LastActivity.getLastActivity(con, "xray@jabber.org");
+ * </pre>
+ *
+ * @author Derek DeMoro
+ */
+public class LastActivity extends IQ {
+
+ public long lastActivity;
+ public String message;
+
+ public LastActivity() {
+ setType(IQ.Type.GET);
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:last\"></query>");
+ return buf.toString();
+ }
+
+
+ private void setLastActivity(long lastActivity) {
+ this.lastActivity = lastActivity;
+ }
+
+
+ private void setMessage(String message) {
+ this.message = message;
+ }
+
+ /**
+ * Returns number of seconds that have passed since the user last logged out.
+ * If the user is offline, 0 will be returned.
+ *
+ * @return the number of seconds that have passed since the user last logged out.
+ */
+ public long getIdleTime() {
+ return lastActivity;
+ }
+
+
+ /**
+ * Returns the status message of the last unavailable presence received from the user.
+ *
+ * @return the status message of the last unavailable presence received from the user
+ */
+ public String getStatusMessage() {
+ return message;
+ }
+
+
+ /**
+ * The IQ Provider for LastActivity.
+ *
+ * @author Derek DeMoro
+ */
+ public static class Provider implements IQProvider {
+
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("Parser not in proper position, or bad XML.");
+ }
+
+ LastActivity lastActivity = new LastActivity();
+ try {
+ String seconds = parser.getAttributeValue("", "seconds");
+ String message = parser.nextText();
+ if (seconds != null) {
+ long xmlSeconds = new Double(seconds).longValue();
+ lastActivity.setLastActivity((int)xmlSeconds);
+ }
+
+ if (message != null) {
+ lastActivity.setMessage(message);
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ return lastActivity;
+ }
+ }
+
+ /**
+ * Retrieve the last activity of a particular jid.
+ * @param con the current XMPPConnection.
+ * @param jid the JID of the user.
+ * @return the LastActivity packet of the jid.
+ * @throws XMPPException thrown if a server error has occured.
+ */
+ public static LastActivity getLastActivity(XMPPConnection con, String jid) throws XMPPException {
+ LastActivity activity = new LastActivity();
+ jid = StringUtils.parseBareAddress(jid);
+ activity.setTo(jid);
+
+ PacketCollector collector = con.createPacketCollector(new PacketIDFilter(activity.getPacketID()));
+ con.sendPacket(activity);
+
+ LastActivity response = (LastActivity) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCAdmin.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCAdmin.java
new file mode 100644
index 000000000..f1e877296
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCAdmin.java
@@ -0,0 +1,234 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+import java.util.*;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * IQ packet that serves for kicking users, granting and revoking voice, banning users,
+ * modifying the ban list, granting and revoking membership and granting and revoking
+ * moderator privileges. All these operations are scoped by the
+ * 'http://jabber.org/protocol/muc#admin' namespace.
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCAdmin extends IQ {
+
+ private List items = new ArrayList();
+
+ /**
+ * Returns an Iterator for item childs that holds information about roles, affiliation,
+ * jids and nicks.
+ *
+ * @return an Iterator for item childs that holds information about roles, affiliation,
+ * jids and nicks.
+ */
+ public Iterator getItems() {
+ synchronized (items) {
+ return Collections.unmodifiableList(new ArrayList(items)).iterator();
+ }
+ }
+
+ /**
+ * Adds an item child that holds information about roles, affiliation, jids and nicks.
+ *
+ * @param item the item child that holds information about roles, affiliation, jids and nicks.
+ */
+ public void addItem(Item item) {
+ synchronized (items) {
+ items.add(item);
+ }
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"http://jabber.org/protocol/muc#admin\">");
+ synchronized (items) {
+ for (int i = 0; i < items.size(); i++) {
+ Item item = (Item) items.get(i);
+ buf.append(item.toXML());
+ }
+ }
+ // Add packet extensions, if any are defined.
+ buf.append(getExtensionsXML());
+ buf.append("</query>");
+ return buf.toString();
+ }
+
+ /**
+ * Item child that holds information about roles, affiliation, jids and nicks.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Item {
+ private String actor;
+ private String reason;
+ private String affiliation;
+ private String jid;
+ private String nick;
+ private String role;
+
+ /**
+ * Creates a new item child.
+ *
+ * @param affiliation the actor's affiliation to the room
+ * @param role the privilege level of an occupant within a room.
+ */
+ public Item(String affiliation, String role) {
+ this.affiliation = affiliation;
+ this.role = role;
+ }
+
+ /**
+ * Returns the actor (JID of an occupant in the room) that was kicked or banned.
+ *
+ * @return the JID of an occupant in the room that was kicked or banned.
+ */
+ public String getActor() {
+ return actor;
+ }
+
+ /**
+ * Returns the reason for the item child. The reason is optional and could be used to
+ * explain the reason why a user (occupant) was kicked or banned.
+ *
+ * @return the reason for the item child.
+ */
+ public String getReason() {
+ return reason;
+ }
+
+ /**
+ * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent
+ * association or connection with a room. The possible affiliations are "owner", "admin",
+ * "member", and "outcast" (naturally it is also possible to have no affiliation). An
+ * affiliation lasts across a user's visits to a room.
+ *
+ * @return the actor's affiliation to the room
+ */
+ public String getAffiliation() {
+ return affiliation;
+ }
+
+ /**
+ * Returns the <room@service/nick> by which an occupant is identified within the context
+ * of a room. If the room is non-anonymous, the JID will be included in the item.
+ *
+ * @return the room JID by which an occupant is identified within the room.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the new nickname of an occupant that is changing his/her nickname. The new
+ * nickname is sent as part of the unavailable presence.
+ *
+ * @return the new nickname of an occupant that is changing his/her nickname.
+ */
+ public String getNick() {
+ return nick;
+ }
+
+ /**
+ * Returns the temporary position or privilege level of an occupant within a room. The
+ * possible roles are "moderator", "participant", and "visitor" (it is also possible to
+ * have no defined role). A role lasts only for the duration of an occupant's visit to
+ * a room.
+ *
+ * @return the privilege level of an occupant within a room.
+ */
+ public String getRole() {
+ return role;
+ }
+
+ /**
+ * Sets the actor (JID of an occupant in the room) that was kicked or banned.
+ *
+ * @param actor the actor (JID of an occupant in the room) that was kicked or banned.
+ */
+ public void setActor(String actor) {
+ this.actor = actor;
+ }
+
+ /**
+ * Sets the reason for the item child. The reason is optional and could be used to
+ * explain the reason why a user (occupant) was kicked or banned.
+ *
+ * @param reason the reason why a user (occupant) was kicked or banned.
+ */
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * Sets the <room@service/nick> by which an occupant is identified within the context
+ * of a room. If the room is non-anonymous, the JID will be included in the item.
+ *
+ * @param jid the JID by which an occupant is identified within a room.
+ */
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ /**
+ * Sets the new nickname of an occupant that is changing his/her nickname. The new
+ * nickname is sent as part of the unavailable presence.
+ *
+ * @param nick the new nickname of an occupant that is changing his/her nickname.
+ */
+ public void setNick(String nick) {
+ this.nick = nick;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<item");
+ if (getAffiliation() != null) {
+ buf.append(" affiliation=\"").append(getAffiliation()).append("\"");
+ }
+ if (getJid() != null) {
+ buf.append(" jid=\"").append(getJid()).append("\"");
+ }
+ if (getNick() != null) {
+ buf.append(" nick=\"").append(getNick()).append("\"");
+ }
+ if (getRole() != null) {
+ buf.append(" role=\"").append(getRole()).append("\"");
+ }
+ if (getReason() == null && getActor() == null) {
+ buf.append("/>");
+ }
+ else {
+ buf.append(">");
+ if (getReason() != null) {
+ buf.append("<reason>").append(getReason()).append("</reason>");
+ }
+ if (getActor() != null) {
+ buf.append("<actor jid=\"").append(getActor()).append("\"/>");
+ }
+ buf.append("</item>");
+ }
+ return buf.toString();
+ }
+ };
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java
new file mode 100644
index 000000000..88ba7f29b
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java
@@ -0,0 +1,223 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents extended presence information whose sole purpose is to signal the ability of
+ * the occupant to speak the MUC protocol when joining a room. If the room requires a password
+ * then the MUCInitialPresence should include one.<p>
+ *
+ * The amount of discussion history provided on entering a room (perhaps because the
+ * user is on a low-bandwidth connection or is using a small-footprint client) could be managed by
+ * setting a configured History instance to the MUCInitialPresence instance.
+ * @see MUCInitialPresence#setHistory(MUCInitialPresence.History).
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCInitialPresence implements PacketExtension {
+
+ private String password;
+ private History history;
+
+ public String getElementName() {
+ return "x";
+ }
+
+ public String getNamespace() {
+ return "http://jabber.org/protocol/muc";
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+ "\">");
+ if (getPassword() != null) {
+ buf.append("<password>").append(getPassword()).append("</password>");
+ }
+ if (getHistory() != null) {
+ buf.append(getHistory().toXML());
+ }
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * Returns the history that manages the amount of discussion history provided on
+ * entering a room.
+ *
+ * @return the history that manages the amount of discussion history provided on
+ * entering a room.
+ */
+ public History getHistory() {
+ return history;
+ }
+
+ /**
+ * Returns the password to use when the room requires a password.
+ *
+ * @return the password to use when the room requires a password.
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * Sets the History that manages the amount of discussion history provided on
+ * entering a room.
+ *
+ * @param history that manages the amount of discussion history provided on
+ * entering a room.
+ */
+ public void setHistory(History history) {
+ this.history = history;
+ }
+
+ /**
+ * Sets the password to use when the room requires a password.
+ *
+ * @param password the password to use when the room requires a password.
+ */
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ /**
+ * The History class controls the number of characters or messages to receive
+ * when entering a room.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class History {
+
+ private int maxChars = -1;
+ private int maxStanzas = -1;
+ private int seconds = -1;
+ private Date since;
+
+ /**
+ * Returns the total number of characters to receive in the history.
+ *
+ * @return total number of characters to receive in the history.
+ */
+ public int getMaxChars() {
+ return maxChars;
+ }
+
+ /**
+ * Returns the total number of messages to receive in the history.
+ *
+ * @return the total number of messages to receive in the history.
+ */
+ public int getMaxStanzas() {
+ return maxStanzas;
+ }
+
+ /**
+ * Returns the number of seconds to use to filter the messages received during that time.
+ * In other words, only the messages received in the last "X" seconds will be included in
+ * the history.
+ *
+ * @return the number of seconds to use to filter the messages received during that time.
+ */
+ public int getSeconds() {
+ return seconds;
+ }
+
+ /**
+ * Returns the since date to use to filter the messages received during that time.
+ * In other words, only the messages received since the datetime specified will be
+ * included in the history.
+ *
+ * @return the since date to use to filter the messages received during that time.
+ */
+ public Date getSince() {
+ return since;
+ }
+
+ /**
+ * Sets the total number of characters to receive in the history.
+ *
+ * @param maxChars the total number of characters to receive in the history.
+ */
+ public void setMaxChars(int maxChars) {
+ this.maxChars = maxChars;
+ }
+
+ /**
+ * Sets the total number of messages to receive in the history.
+ *
+ * @param maxStanzas the total number of messages to receive in the history.
+ */
+ public void setMaxStanzas(int maxStanzas) {
+ this.maxStanzas = maxStanzas;
+ }
+
+ /**
+ * Sets the number of seconds to use to filter the messages received during that time.
+ * In other words, only the messages received in the last "X" seconds will be included in
+ * the history.
+ *
+ * @param seconds the number of seconds to use to filter the messages received during
+ * that time.
+ */
+ public void setSeconds(int seconds) {
+ this.seconds = seconds;
+ }
+
+ /**
+ * Sets the since date to use to filter the messages received during that time.
+ * In other words, only the messages received since the datetime specified will be
+ * included in the history.
+ *
+ * @param since the since date to use to filter the messages received during that time.
+ */
+ public void setSince(Date since) {
+ this.since = since;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<history");
+ if (getMaxChars() != -1) {
+ buf.append(" maxchars=\"").append(getMaxChars()).append("\"");
+ }
+ if (getMaxStanzas() != -1) {
+ buf.append(" maxstanzas=\"").append(getMaxStanzas()).append("\"");
+ }
+ if (getSeconds() != -1) {
+ buf.append(" seconds=\"").append(getSeconds()).append("\"");
+ }
+ if (getSince() != null) {
+ SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+ utcFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ buf.append(" since=\"").append(utcFormat.format(getSince())).append("\"");
+ }
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCOwner.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCOwner.java
new file mode 100644
index 000000000..626746884
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCOwner.java
@@ -0,0 +1,339 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+import java.util.*;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * IQ packet that serves for granting and revoking ownership privileges, granting
+ * and revoking administrative privileges and destroying a room. All these operations
+ * are scoped by the 'http://jabber.org/protocol/muc#owner' namespace.
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCOwner extends IQ {
+
+ private List items = new ArrayList();
+ private Destroy destroy;
+
+ /**
+ * Returns an Iterator for item childs that holds information about affiliation,
+ * jids and nicks.
+ *
+ * @return an Iterator for item childs that holds information about affiliation,
+ * jids and nicks.
+ */
+ public Iterator getItems() {
+ synchronized (items) {
+ return Collections.unmodifiableList(new ArrayList(items)).iterator();
+ }
+ }
+
+ /**
+ * Returns a request to the server to destroy a room. The sender of the request
+ * should be the room's owner. If the sender of the destroy request is not the room's owner
+ * then the server will answer a "Forbidden" error.
+ *
+ * @return a request to the server to destroy a room.
+ */
+ public Destroy getDestroy() {
+ return destroy;
+ }
+
+ /**
+ * Sets a request to the server to destroy a room. The sender of the request
+ * should be the room's owner. If the sender of the destroy request is not the room's owner
+ * then the server will answer a "Forbidden" error.
+ *
+ * @param destroy the request to the server to destroy a room.
+ */
+ public void setDestroy(Destroy destroy) {
+ this.destroy = destroy;
+ }
+
+ /**
+ * Adds an item child that holds information about affiliation, jids and nicks.
+ *
+ * @param item the item child that holds information about affiliation, jids and nicks.
+ */
+ public void addItem(Item item) {
+ synchronized (items) {
+ items.add(item);
+ }
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"http://jabber.org/protocol/muc#owner\">");
+ synchronized (items) {
+ for (int i = 0; i < items.size(); i++) {
+ Item item = (Item) items.get(i);
+ buf.append(item.toXML());
+ }
+ }
+ if (getDestroy() != null) {
+ buf.append(getDestroy().toXML());
+ }
+ // Add packet extensions, if any are defined.
+ buf.append(getExtensionsXML());
+ buf.append("</query>");
+ return buf.toString();
+ }
+
+ /**
+ * Item child that holds information about affiliation, jids and nicks.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Item {
+
+ private String actor;
+ private String reason;
+ private String affiliation;
+ private String jid;
+ private String nick;
+ private String role;
+
+ /**
+ * Creates a new item child.
+ *
+ * @param affiliation the actor's affiliation to the room
+ */
+ public Item(String affiliation) {
+ this.affiliation = affiliation;
+ }
+
+ /**
+ * Returns the actor (JID of an occupant in the room) that was kicked or banned.
+ *
+ * @return the JID of an occupant in the room that was kicked or banned.
+ */
+ public String getActor() {
+ return actor;
+ }
+
+ /**
+ * Returns the reason for the item child. The reason is optional and could be used to
+ * explain the reason why a user (occupant) was kicked or banned.
+ *
+ * @return the reason for the item child.
+ */
+ public String getReason() {
+ return reason;
+ }
+
+ /**
+ * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent
+ * association or connection with a room. The possible affiliations are "owner", "admin",
+ * "member", and "outcast" (naturally it is also possible to have no affiliation). An
+ * affiliation lasts across a user's visits to a room.
+ *
+ * @return the actor's affiliation to the room
+ */
+ public String getAffiliation() {
+ return affiliation;
+ }
+
+ /**
+ * Returns the <room@service/nick> by which an occupant is identified within the context
+ * of a room. If the room is non-anonymous, the JID will be included in the item.
+ *
+ * @return the room JID by which an occupant is identified within the room.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the new nickname of an occupant that is changing his/her nickname. The new
+ * nickname is sent as part of the unavailable presence.
+ *
+ * @return the new nickname of an occupant that is changing his/her nickname.
+ */
+ public String getNick() {
+ return nick;
+ }
+
+ /**
+ * Returns the temporary position or privilege level of an occupant within a room. The
+ * possible roles are "moderator", "participant", and "visitor" (it is also possible to
+ * have no defined role). A role lasts only for the duration of an occupant's visit to
+ * a room.
+ *
+ * @return the privilege level of an occupant within a room.
+ */
+ public String getRole() {
+ return role;
+ }
+
+ /**
+ * Sets the actor (JID of an occupant in the room) that was kicked or banned.
+ *
+ * @param actor the actor (JID of an occupant in the room) that was kicked or banned.
+ */
+ public void setActor(String actor) {
+ this.actor = actor;
+ }
+
+ /**
+ * Sets the reason for the item child. The reason is optional and could be used to
+ * explain the reason why a user (occupant) was kicked or banned.
+ *
+ * @param reason the reason why a user (occupant) was kicked or banned.
+ */
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * Sets the <room@service/nick> by which an occupant is identified within the context
+ * of a room. If the room is non-anonymous, the JID will be included in the item.
+ *
+ * @param jid the JID by which an occupant is identified within a room.
+ */
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ /**
+ * Sets the new nickname of an occupant that is changing his/her nickname. The new
+ * nickname is sent as part of the unavailable presence.
+ *
+ * @param nick the new nickname of an occupant that is changing his/her nickname.
+ */
+ public void setNick(String nick) {
+ this.nick = nick;
+ }
+
+ /**
+ * Sets the temporary position or privilege level of an occupant within a room. The
+ * possible roles are "moderator", "participant", and "visitor" (it is also possible to
+ * have no defined role). A role lasts only for the duration of an occupant's visit to
+ * a room.
+ *
+ * @param role the new privilege level of an occupant within a room.
+ */
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<item");
+ if (getAffiliation() != null) {
+ buf.append(" affiliation=\"").append(getAffiliation()).append("\"");
+ }
+ if (getJid() != null) {
+ buf.append(" jid=\"").append(getJid()).append("\"");
+ }
+ if (getNick() != null) {
+ buf.append(" nick=\"").append(getNick()).append("\"");
+ }
+ if (getRole() != null) {
+ buf.append(" role=\"").append(getRole()).append("\"");
+ }
+ if (getReason() == null && getActor() == null) {
+ buf.append("/>");
+ }
+ else {
+ buf.append(">");
+ if (getReason() != null) {
+ buf.append("<reason>").append(getReason()).append("</reason>");
+ }
+ if (getActor() != null) {
+ buf.append("<actor jid=\"").append(getActor()).append("\"/>");
+ }
+ buf.append("</item>");
+ }
+ return buf.toString();
+ }
+ };
+
+ /**
+ * Represents a request to the server to destroy a room. The sender of the request
+ * should be the room's owner. If the sender of the destroy request is not the room's owner
+ * then the server will answer a "Forbidden" error.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Destroy {
+ private String reason;
+ private String jid;
+
+
+ /**
+ * Returns the JID of an alternate location since the current room is being destroyed.
+ *
+ * @return the JID of an alternate location.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the reason for the room destruction.
+ *
+ * @return the reason for the room destruction.
+ */
+ public String getReason() {
+ return reason;
+ }
+
+ /**
+ * Sets the JID of an alternate location since the current room is being destroyed.
+ *
+ * @param jid the JID of an alternate location.
+ */
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ /**
+ * Sets the reason for the room destruction.
+ *
+ * @param reason the reason for the room destruction.
+ */
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<destroy");
+ if (getJid() != null) {
+ buf.append(" jid=\"").append(getJid()).append("\"");
+ }
+ if (getReason() == null) {
+ buf.append("/>");
+ }
+ else {
+ buf.append(">");
+ if (getReason() != null) {
+ buf.append("<reason>").append(getReason()).append("</reason>");
+ }
+ buf.append("</destroy>");
+ }
+ return buf.toString();
+ }
+
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCUser.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCUser.java
new file mode 100644
index 000000000..7e84cea6b
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCUser.java
@@ -0,0 +1,627 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents extended presence information about roles, affiliations, full JIDs,
+ * or status codes scoped by the 'http://jabber.org/protocol/muc#user' namespace.
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCUser implements PacketExtension {
+
+ private Invite invite;
+ private Decline decline;
+ private Item item;
+ private String password;
+ private Status status;
+ private Destroy destroy;
+
+ public String getElementName() {
+ return "x";
+ }
+
+ public String getNamespace() {
+ return "http://jabber.org/protocol/muc#user";
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+ "\">");
+ if (getInvite() != null) {
+ buf.append(getInvite().toXML());
+ }
+ if (getDecline() != null) {
+ buf.append(getDecline().toXML());
+ }
+ if (getItem() != null) {
+ buf.append(getItem().toXML());
+ }
+ if (getPassword() != null) {
+ buf.append("<password>").append(getPassword()).append("</password>");
+ }
+ if (getStatus() != null) {
+ buf.append(getStatus().toXML());
+ }
+ if (getDestroy() != null) {
+ buf.append(getDestroy().toXML());
+ }
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * Returns the invitation for another user to a room. The sender of the invitation
+ * must be an occupant of the room. The invitation will be sent to the room which in turn
+ * will forward the invitation to the invitee.
+ *
+ * @return an invitation for another user to a room.
+ */
+ public Invite getInvite() {
+ return invite;
+ }
+
+ /**
+ * Returns the rejection to an invitation from another user to a room. The rejection will be
+ * sent to the room which in turn will forward the refusal to the inviter.
+ *
+ * @return a rejection to an invitation from another user to a room.
+ */
+ public Decline getDecline() {
+ return decline;
+ }
+
+ /**
+ * Returns the item child that holds information about roles, affiliation, jids and nicks.
+ *
+ * @return an item child that holds information about roles, affiliation, jids and nicks.
+ */
+ public Item getItem() {
+ return item;
+ }
+
+ /**
+ * Returns the password to use to enter Password-Protected Room. A Password-Protected Room is
+ * a room that a user cannot enter without first providing the correct password.
+ *
+ * @return the password to use to enter Password-Protected Room.
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * Returns the status which holds a code that assists in presenting notification messages.
+ *
+ * @return the status which holds a code that assists in presenting notification messages.
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ /**
+ * Returns the notification that the room has been destroyed. After a room has been destroyed,
+ * the room occupants will receive a Presence packet of type 'unavailable' with the reason for
+ * the room destruction if provided by the room owner.
+ *
+ * @return a notification that the room has been destroyed.
+ */
+ public Destroy getDestroy() {
+ return destroy;
+ }
+
+ /**
+ * Sets the invitation for another user to a room. The sender of the invitation
+ * must be an occupant of the room. The invitation will be sent to the room which in turn
+ * will forward the invitation to the invitee.
+ *
+ * @param invite the invitation for another user to a room.
+ */
+ public void setInvite(Invite invite) {
+ this.invite = invite;
+ }
+
+ /**
+ * Sets the rejection to an invitation from another user to a room. The rejection will be
+ * sent to the room which in turn will forward the refusal to the inviter.
+ *
+ * @param decline the rejection to an invitation from another user to a room.
+ */
+ public void setDecline(Decline decline) {
+ this.decline = decline;
+ }
+
+ /**
+ * Sets the item child that holds information about roles, affiliation, jids and nicks.
+ *
+ * @param item the item child that holds information about roles, affiliation, jids and nicks.
+ */
+ public void setItem(Item item) {
+ this.item = item;
+ }
+
+ /**
+ * Sets the password to use to enter Password-Protected Room. A Password-Protected Room is
+ * a room that a user cannot enter without first providing the correct password.
+ *
+ * @param string the password to use to enter Password-Protected Room.
+ */
+ public void setPassword(String string) {
+ password = string;
+ }
+
+ /**
+ * Sets the status which holds a code that assists in presenting notification messages.
+ *
+ * @param status the status which holds a code that assists in presenting notification
+ * messages.
+ */
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ /**
+ * Sets the notification that the room has been destroyed. After a room has been destroyed,
+ * the room occupants will receive a Presence packet of type 'unavailable' with the reason for
+ * the room destruction if provided by the room owner.
+ *
+ * @param destroy the notification that the room has been destroyed.
+ */
+ public void setDestroy(Destroy destroy) {
+ this.destroy = destroy;
+ }
+
+ /**
+ * Represents an invitation for another user to a room. The sender of the invitation
+ * must be an occupant of the room. The invitation will be sent to the room which in turn
+ * will forward the invitation to the invitee.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Invite {
+ private String reason;
+ private String from;
+ private String to;
+
+ /**
+ * Returns the bare JID of the inviter or, optionally, the room JID. (e.g.
+ * 'crone1@shakespeare.lit/desktop').
+ *
+ * @return the room's occupant that sent the invitation.
+ */
+ public String getFrom() {
+ return from;
+ }
+
+ /**
+ * Returns the message explaining the invitation.
+ *
+ * @return the message explaining the invitation.
+ */
+ public String getReason() {
+ return reason;
+ }
+
+ /**
+ * Returns the bare JID of the invitee. (e.g. 'hecate@shakespeare.lit')
+ *
+ * @return the bare JID of the invitee.
+ */
+ public String getTo() {
+ return to;
+ }
+
+ /**
+ * Sets the bare JID of the inviter or, optionally, the room JID. (e.g.
+ * 'crone1@shakespeare.lit/desktop')
+ *
+ * @param from the bare JID of the inviter or, optionally, the room JID.
+ */
+ public void setFrom(String from) {
+ this.from = from;
+ }
+
+ /**
+ * Sets the message explaining the invitation.
+ *
+ * @param reason the message explaining the invitation.
+ */
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * Sets the bare JID of the invitee. (e.g. 'hecate@shakespeare.lit')
+ *
+ * @param to the bare JID of the invitee.
+ */
+ public void setTo(String to) {
+ this.to = to;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<invite ");
+ if (getTo() != null) {
+ buf.append(" to=\"").append(getTo()).append("\"");
+ }
+ if (getFrom() != null) {
+ buf.append(" from=\"").append(getFrom()).append("\"");
+ }
+ buf.append(">");
+ if (getReason() != null) {
+ buf.append("<reason>").append(getReason()).append("</reason>");
+ }
+ buf.append("</invite>");
+ return buf.toString();
+ }
+ };
+
+ /**
+ * Represents a rejection to an invitation from another user to a room. The rejection will be
+ * sent to the room which in turn will forward the refusal to the inviter.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Decline {
+ private String reason;
+ private String from;
+ private String to;
+
+ /**
+ * Returns the bare JID of the invitee that rejected the invitation. (e.g.
+ * 'crone1@shakespeare.lit/desktop').
+ *
+ * @return the bare JID of the invitee that rejected the invitation.
+ */
+ public String getFrom() {
+ return from;
+ }
+
+ /**
+ * Returns the message explaining why the invitation was rejected.
+ *
+ * @return the message explaining the reason for the rejection.
+ */
+ public String getReason() {
+ return reason;
+ }
+
+ /**
+ * Returns the bare JID of the inviter. (e.g. 'hecate@shakespeare.lit')
+ *
+ * @return the bare JID of the inviter.
+ */
+ public String getTo() {
+ return to;
+ }
+
+ /**
+ * Sets the bare JID of the invitee that rejected the invitation. (e.g.
+ * 'crone1@shakespeare.lit/desktop').
+ *
+ * @param from the bare JID of the invitee that rejected the invitation.
+ */
+ public void setFrom(String from) {
+ this.from = from;
+ }
+
+ /**
+ * Sets the message explaining why the invitation was rejected.
+ *
+ * @param reason the message explaining the reason for the rejection.
+ */
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * Sets the bare JID of the inviter. (e.g. 'hecate@shakespeare.lit')
+ *
+ * @param to the bare JID of the inviter.
+ */
+ public void setTo(String to) {
+ this.to = to;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<decline ");
+ if (getTo() != null) {
+ buf.append(" to=\"").append(getTo()).append("\"");
+ }
+ if (getFrom() != null) {
+ buf.append(" from=\"").append(getFrom()).append("\"");
+ }
+ buf.append(">");
+ if (getReason() != null) {
+ buf.append("<reason>").append(getReason()).append("</reason>");
+ }
+ buf.append("</decline>");
+ return buf.toString();
+ }
+ };
+
+ /**
+ * Item child that holds information about roles, affiliation, jids and nicks.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Item {
+ private String actor;
+ private String reason;
+ private String affiliation;
+ private String jid;
+ private String nick;
+ private String role;
+
+ /**
+ * Creates a new item child.
+ *
+ * @param affiliation the actor's affiliation to the room
+ * @param role the privilege level of an occupant within a room.
+ */
+ public Item(String affiliation, String role) {
+ this.affiliation = affiliation;
+ this.role = role;
+ }
+
+ /**
+ * Returns the actor (JID of an occupant in the room) that was kicked or banned.
+ *
+ * @return the JID of an occupant in the room that was kicked or banned.
+ */
+ public String getActor() {
+ return actor == null ? "" : actor;
+ }
+
+ /**
+ * Returns the reason for the item child. The reason is optional and could be used to
+ * explain the reason why a user (occupant) was kicked or banned.
+ *
+ * @return the reason for the item child.
+ */
+ public String getReason() {
+ return reason == null ? "" : reason;
+ }
+
+ /**
+ * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent
+ * association or connection with a room. The possible affiliations are "owner", "admin",
+ * "member", and "outcast" (naturally it is also possible to have no affiliation). An
+ * affiliation lasts across a user's visits to a room.
+ *
+ * @return the actor's affiliation to the room
+ */
+ public String getAffiliation() {
+ return affiliation;
+ }
+
+ /**
+ * Returns the <room@service/nick> by which an occupant is identified within the context
+ * of a room. If the room is non-anonymous, the JID will be included in the item.
+ *
+ * @return the room JID by which an occupant is identified within the room.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the new nickname of an occupant that is changing his/her nickname. The new
+ * nickname is sent as part of the unavailable presence.
+ *
+ * @return the new nickname of an occupant that is changing his/her nickname.
+ */
+ public String getNick() {
+ return nick;
+ }
+
+ /**
+ * Returns the temporary position or privilege level of an occupant within a room. The
+ * possible roles are "moderator", "participant", and "visitor" (it is also possible to
+ * have no defined role). A role lasts only for the duration of an occupant's visit to
+ * a room.
+ *
+ * @return the privilege level of an occupant within a room.
+ */
+ public String getRole() {
+ return role;
+ }
+
+ /**
+ * Sets the actor (JID of an occupant in the room) that was kicked or banned.
+ *
+ * @param actor the actor (JID of an occupant in the room) that was kicked or banned.
+ */
+ public void setActor(String actor) {
+ this.actor = actor;
+ }
+
+ /**
+ * Sets the reason for the item child. The reason is optional and could be used to
+ * explain the reason why a user (occupant) was kicked or banned.
+ *
+ * @param reason the reason why a user (occupant) was kicked or banned.
+ */
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * Sets the <room@service/nick> by which an occupant is identified within the context
+ * of a room. If the room is non-anonymous, the JID will be included in the item.
+ *
+ * @param jid the JID by which an occupant is identified within a room.
+ */
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ /**
+ * Sets the new nickname of an occupant that is changing his/her nickname. The new
+ * nickname is sent as part of the unavailable presence.
+ *
+ * @param nick the new nickname of an occupant that is changing his/her nickname.
+ */
+ public void setNick(String nick) {
+ this.nick = nick;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<item");
+ if (getAffiliation() != null) {
+ buf.append(" affiliation=\"").append(getAffiliation()).append("\"");
+ }
+ if (getJid() != null) {
+ buf.append(" jid=\"").append(getJid()).append("\"");
+ }
+ if (getNick() != null) {
+ buf.append(" nick=\"").append(getNick()).append("\"");
+ }
+ if (getRole() != null) {
+ buf.append(" role=\"").append(getRole()).append("\"");
+ }
+ if (getReason() == null && getActor() == null) {
+ buf.append("/>");
+ }
+ else {
+ buf.append(">");
+ if (getReason() != null) {
+ buf.append("<reason>").append(getReason()).append("</reason>");
+ }
+ if (getActor() != null) {
+ buf.append("<actor jid=\"").append(getActor()).append("\"/>");
+ }
+ buf.append("</item>");
+ }
+ return buf.toString();
+ }
+ };
+
+ /**
+ * Status code assists in presenting notification messages. The following link provides the
+ * list of existing error codes (@link http://www.jabber.org/jeps/jep-0045.html#errorstatus).
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Status {
+ private String code;
+
+ /**
+ * Creates a new instance of Status with the specified code.
+ *
+ * @param code the code that uniquely identifies the reason of the error.
+ */
+ public Status(String code) {
+ this.code = code;
+ }
+
+ /**
+ * Returns the code that uniquely identifies the reason of the error. The code
+ * assists in presenting notification messages.
+ *
+ * @return the code that uniquely identifies the reason of the error.
+ */
+ public String getCode() {
+ return code;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<status code=\"").append(getCode()).append("\"/>");
+ return buf.toString();
+ }
+ };
+
+ /**
+ * Represents a notification that the room has been destroyed. After a room has been destroyed,
+ * the room occupants will receive a Presence packet of type 'unavailable' with the reason for
+ * the room destruction if provided by the room owner.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Destroy {
+ private String reason;
+ private String jid;
+
+
+ /**
+ * Returns the JID of an alternate location since the current room is being destroyed.
+ *
+ * @return the JID of an alternate location.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Returns the reason for the room destruction.
+ *
+ * @return the reason for the room destruction.
+ */
+ public String getReason() {
+ return reason;
+ }
+
+ /**
+ * Sets the JID of an alternate location since the current room is being destroyed.
+ *
+ * @param jid the JID of an alternate location.
+ */
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ /**
+ * Sets the reason for the room destruction.
+ *
+ * @param reason the reason for the room destruction.
+ */
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<destroy");
+ if (getJid() != null) {
+ buf.append(" jid=\"").append(getJid()).append("\"");
+ }
+ if (getReason() == null) {
+ buf.append("/>");
+ }
+ else {
+ buf.append(">");
+ if (getReason() != null) {
+ buf.append("<reason>").append(getReason()).append("</reason>");
+ }
+ buf.append("</destroy>");
+ }
+ return buf.toString();
+ }
+
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MessageEvent.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MessageEvent.java
new file mode 100644
index 000000000..617e9d14e
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MessageEvent.java
@@ -0,0 +1,334 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.util.*;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents message events relating to the delivery, display, composition and cancellation of
+ * messages.<p>
+ *
+ * There are four message events currently defined in this namespace:
+ * <ol>
+ * <li>Offline<br>
+ * Indicates that the message has been stored offline by the intended recipient's server. This
+ * event is triggered only if the intended recipient's server supports offline storage, has that
+ * support enabled, and the recipient is offline when the server receives the message for delivery.</li>
+ *
+ * <li>Delivered<br>
+ * Indicates that the message has been delivered to the recipient. This signifies that the message
+ * has reached the recipient's XMPP client, but does not necessarily mean that the message has
+ * been displayed. This event is to be raised by the XMPP client.</li>
+ *
+ * <li>Displayed<br>
+ * Once the message has been received by the recipient's XMPP client, it may be displayed to the
+ * user. This event indicates that the message has been displayed, and is to be raised by the
+ * XMPP client. Even if a message is displayed multiple times, this event should be raised only
+ * once.</li>
+ *
+ * <li>Composing<br>
+ * In threaded chat conversations, this indicates that the recipient is composing a reply to a
+ * message. The event is to be raised by the recipient's XMPP client. A XMPP client is allowed
+ * to raise this event multiple times in response to the same request, providing the original
+ * event is cancelled first.</li>
+ * </ol>
+ *
+ * @author Gaston Dombiak
+ */
+public class MessageEvent implements PacketExtension {
+
+ public static final String OFFLINE = "offline";
+ public static final String COMPOSING = "composing";
+ public static final String DISPLAYED = "displayed";
+ public static final String DELIVERED = "delivered";
+ public static final String CANCELLED = "cancelled";
+
+ private boolean offline = false;
+ private boolean delivered = false;
+ private boolean displayed = false;
+ private boolean composing = false;
+ private boolean cancelled = true;
+
+ private String packetID = null;
+
+ /**
+ * Returns the XML element name of the extension sub-packet root element.
+ * Always returns "x"
+ *
+ * @return the XML element name of the packet extension.
+ */
+ public String getElementName() {
+ return "x";
+ }
+
+ /**
+ * Returns the XML namespace of the extension sub-packet root element.
+ * According the specification the namespace is always "jabber:x:event"
+ *
+ * @return the XML namespace of the packet extension.
+ */
+ public String getNamespace() {
+ return "jabber:x:event";
+ }
+
+ /**
+ * When the message is a request returns if the sender of the message requests to be notified
+ * when the receiver is composing a reply.
+ * When the message is a notification returns if the receiver of the message is composing a
+ * reply.
+ *
+ * @return true if the sender is requesting to be notified when composing or when notifying
+ * that the receiver of the message is composing a reply
+ */
+ public boolean isComposing() {
+ return composing;
+ }
+
+ /**
+ * When the message is a request returns if the sender of the message requests to be notified
+ * when the message is delivered.
+ * When the message is a notification returns if the message was delivered or not.
+ *
+ * @return true if the sender is requesting to be notified when delivered or when notifying
+ * that the message was delivered
+ */
+ public boolean isDelivered() {
+ return delivered;
+ }
+
+ /**
+ * When the message is a request returns if the sender of the message requests to be notified
+ * when the message is displayed.
+ * When the message is a notification returns if the message was displayed or not.
+ *
+ * @return true if the sender is requesting to be notified when displayed or when notifying
+ * that the message was displayed
+ */
+ public boolean isDisplayed() {
+ return displayed;
+ }
+
+ /**
+ * When the message is a request returns if the sender of the message requests to be notified
+ * when the receiver of the message is offline.
+ * When the message is a notification returns if the receiver of the message was offline.
+ *
+ * @return true if the sender is requesting to be notified when offline or when notifying
+ * that the receiver of the message is offline
+ */
+ public boolean isOffline() {
+ return offline;
+ }
+
+ /**
+ * When the message is a notification returns if the receiver of the message cancelled
+ * composing a reply.
+ *
+ * @return true if the receiver of the message cancelled composing a reply
+ */
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ /**
+ * Returns the unique ID of the message that requested to be notified of the event.
+ * The packet id is not used when the message is a request for notifications
+ *
+ * @return the message id that requested to be notified of the event.
+ */
+ public String getPacketID() {
+ return packetID;
+ }
+
+ /**
+ * Returns the types of events. The type of event could be:
+ * "offline", "composing","delivered","displayed", "offline"
+ *
+ * @return an iterator over all the types of events of the MessageEvent.
+ */
+ public Iterator getEventTypes() {
+ ArrayList allEvents = new ArrayList();
+ if (isDelivered()) {
+ allEvents.add(MessageEvent.DELIVERED);
+ }
+ if (!isMessageEventRequest() && isCancelled()) {
+ allEvents.add(MessageEvent.CANCELLED);
+ }
+ if (isComposing()) {
+ allEvents.add(MessageEvent.COMPOSING);
+ }
+ if (isDisplayed()) {
+ allEvents.add(MessageEvent.DISPLAYED);
+ }
+ if (isOffline()) {
+ allEvents.add(MessageEvent.OFFLINE);
+ }
+ return allEvents.iterator();
+ }
+
+ /**
+ * When the message is a request sets if the sender of the message requests to be notified
+ * when the receiver is composing a reply.
+ * When the message is a notification sets if the receiver of the message is composing a
+ * reply.
+ *
+ * @param composing sets if the sender is requesting to be notified when composing or when
+ * notifying that the receiver of the message is composing a reply
+ */
+ public void setComposing(boolean composing) {
+ this.composing = composing;
+ setCancelled(false);
+ }
+
+ /**
+ * When the message is a request sets if the sender of the message requests to be notified
+ * when the message is delivered.
+ * When the message is a notification sets if the message was delivered or not.
+ *
+ * @param delivered sets if the sender is requesting to be notified when delivered or when
+ * notifying that the message was delivered
+ */
+ public void setDelivered(boolean delivered) {
+ this.delivered = delivered;
+ setCancelled(false);
+ }
+
+ /**
+ * When the message is a request sets if the sender of the message requests to be notified
+ * when the message is displayed.
+ * When the message is a notification sets if the message was displayed or not.
+ *
+ * @param displayed sets if the sender is requesting to be notified when displayed or when
+ * notifying that the message was displayed
+ */
+ public void setDisplayed(boolean displayed) {
+ this.displayed = displayed;
+ setCancelled(false);
+ }
+
+ /**
+ * When the message is a request sets if the sender of the message requests to be notified
+ * when the receiver of the message is offline.
+ * When the message is a notification sets if the receiver of the message was offline.
+ *
+ * @param offline sets if the sender is requesting to be notified when offline or when
+ * notifying that the receiver of the message is offline
+ */
+ public void setOffline(boolean offline) {
+ this.offline = offline;
+ setCancelled(false);
+ }
+
+ /**
+ * When the message is a notification sets if the receiver of the message cancelled
+ * composing a reply.
+ * The Cancelled event is never requested explicitly. It is requested implicitly when
+ * requesting to be notified of the Composing event.
+ *
+ * @param cancelled sets if the receiver of the message cancelled composing a reply
+ */
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ /**
+ * Sets the unique ID of the message that requested to be notified of the event.
+ * The packet id is not used when the message is a request for notifications
+ *
+ * @param packetID the message id that requested to be notified of the event.
+ */
+ public void setPacketID(String packetID) {
+ this.packetID = packetID;
+ }
+
+ /**
+ * Returns true if this MessageEvent is a request for notifications.
+ * Returns false if this MessageEvent is a notification of an event.
+ *
+ * @return true if this message is a request for notifications.
+ */
+ public boolean isMessageEventRequest() {
+ return this.packetID == null;
+ }
+
+ /**
+ * Returns the XML representation of a Message Event according the specification.
+ *
+ * Usually the XML representation will be inside of a Message XML representation like
+ * in the following examples:<p>
+ *
+ * Request to be notified when displayed:
+ * <pre>
+ * &lt;message
+ * to='romeo@montague.net/orchard'
+ * from='juliet@capulet.com/balcony'
+ * id='message22'&gt;
+ * &lt;x xmlns='jabber:x:event'&gt;
+ * &lt;displayed/&gt;
+ * &lt;/x&gt;
+ * &lt;/message&gt;
+ * </pre>
+ *
+ * Notification of displayed:
+ * <pre>
+ * &lt;message
+ * from='romeo@montague.net/orchard'
+ * to='juliet@capulet.com/balcony'&gt;
+ * &lt;x xmlns='jabber:x:event'&gt;
+ * &lt;displayed/&gt;
+ * &lt;id&gt;message22&lt;/id&gt;
+ * &lt;/x&gt;
+ * &lt;/message&gt;
+ * </pre>
+ *
+ */
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+ "\">");
+ // Note: Cancellation events don't specify any tag. They just send the packetID
+
+ // Add the offline tag if the sender requests to be notified of offline events or if
+ // the target is offline
+ if (isOffline())
+ buf.append("<").append(MessageEvent.OFFLINE).append("/>");
+ // Add the delivered tag if the sender requests to be notified when the message is
+ // delivered or if the target notifies that the message has been delivered
+ if (isDelivered())
+ buf.append("<").append(MessageEvent.DELIVERED).append("/>");
+ // Add the displayed tag if the sender requests to be notified when the message is
+ // displayed or if the target notifies that the message has been displayed
+ if (isDisplayed())
+ buf.append("<").append(MessageEvent.DISPLAYED).append("/>");
+ // Add the composing tag if the sender requests to be notified when the target is
+ // composing a reply or if the target notifies that he/she is composing a reply
+ if (isComposing())
+ buf.append("<").append(MessageEvent.COMPOSING).append("/>");
+ // Add the id tag only if the MessageEvent is a notification message (not a request)
+ if (getPacketID() != null)
+ buf.append("<id>").append(getPacketID()).append("</id>");
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MultipleAddresses.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MultipleAddresses.java
new file mode 100644
index 000000000..1222fc3cd
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MultipleAddresses.java
@@ -0,0 +1,205 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Packet extension that contains the list of addresses that a packet should be sent or was sent.
+ *
+ * @author Gaston Dombiak
+ */
+public class MultipleAddresses implements PacketExtension {
+
+ public static final String BCC = "bcc";
+ public static final String CC = "cc";
+ public static final String NO_REPLY = "noreply";
+ public static final String REPLY_ROOM = "replyroom";
+ public static final String REPLY_TO = "replyto";
+ public static final String TO = "to";
+
+
+ private List addresses = new ArrayList();
+
+ /**
+ * Adds a new address to which the packet is going to be sent or was sent.
+ *
+ * @param type on of the static type (BCC, CC, NO_REPLY, REPLY_ROOM, etc.)
+ * @param jid the JID address of the recipient.
+ * @param node used to specify a sub-addressable unit at a particular JID, corresponding to
+ * a Service Discovery node.
+ * @param desc used to specify human-readable information for this address.
+ * @param delivered true when the packet was already delivered to this address.
+ * @param uri used to specify an external system address, such as a sip:, sips:, or im: URI.
+ */
+ public void addAddress(String type, String jid, String node, String desc, boolean delivered,
+ String uri) {
+ // Create a new address with the specificed configuration
+ Address address = new Address(type);
+ address.setJid(jid);
+ address.setNode(node);
+ address.setDescription(desc);
+ address.setDelivered(delivered);
+ address.setUri(uri);
+ // Add the new address to the list of multiple recipients
+ addresses.add(address);
+ }
+
+ /**
+ * Indicate that the packet being sent should not be replied.
+ */
+ public void setNoReply() {
+ // Create a new address with the specificed configuration
+ Address address = new Address(NO_REPLY);
+ // Add the new address to the list of multiple recipients
+ addresses.add(address);
+ }
+
+ /**
+ * Returns the list of addresses that matches the specified type. Examples of address
+ * type are: TO, CC, BCC, etc..
+ *
+ * @param type Examples of address type are: TO, CC, BCC, etc.
+ * @return the list of addresses that matches the specified type.
+ */
+ public List getAddressesOfType(String type) {
+ List answer = new ArrayList(addresses.size());
+ for (Iterator it = addresses.iterator(); it.hasNext();) {
+ Address address = (Address) it.next();
+ if (address.getType().equals(type)) {
+ answer.add(address);
+ }
+ }
+
+ return answer;
+ }
+
+ public String getElementName() {
+ return "addresses";
+ }
+
+ public String getNamespace() {
+ return "http://jabber.org/protocol/address";
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName());
+ buf.append(" xmlns=\"").append(getNamespace()).append("\">");
+ // Loop through all the addresses and append them to the string buffer
+ for (Iterator i = addresses.iterator(); i.hasNext();) {
+ Address address = (Address) i.next();
+ buf.append(address.toXML());
+ }
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ public static class Address {
+
+ private String type;
+ private String jid;
+ private String node;
+ private String description;
+ private boolean delivered;
+ private String uri;
+
+ private Address(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getJid() {
+ return jid;
+ }
+
+ private void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ public String getNode() {
+ return node;
+ }
+
+ private void setNode(String node) {
+ this.node = node;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ private void setDescription(String description) {
+ this.description = description;
+ }
+
+ public boolean isDelivered() {
+ return delivered;
+ }
+
+ private void setDelivered(boolean delivered) {
+ this.delivered = delivered;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ private void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ private String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<address type=\"");
+ // Append the address type (e.g. TO/CC/BCC)
+ buf.append(type).append("\"");
+ if (jid != null) {
+ buf.append(" jid=\"");
+ buf.append(jid).append("\"");
+ }
+ if (node != null) {
+ buf.append(" node=\"");
+ buf.append(node).append("\"");
+ }
+ if (description != null && description.trim().length() > 0) {
+ buf.append(" desc=\"");
+ buf.append(description).append("\"");
+ }
+ if (delivered) {
+ buf.append(" delivered=\"true\"");
+ }
+ if (uri != null) {
+ buf.append(" uri=\"");
+ buf.append(uri).append("\"");
+ }
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java
new file mode 100644
index 000000000..90c1ead71
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java
@@ -0,0 +1,128 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * OfflineMessageInfo is an extension included in the retrieved offline messages requested by
+ * the {@link org.jivesoftware.smackx.OfflineMessageManager}. This extension includes a stamp
+ * that uniquely identifies the offline message. This stamp may be used for deleting the offline
+ * message. The stamp may be of the form UTC timestamps but it is not required to have that format.
+ *
+ * @author Gaston Dombiak
+ */
+public class OfflineMessageInfo implements PacketExtension {
+
+ private String node = null;
+
+ /**
+ * Returns the XML element name of the extension sub-packet root element.
+ * Always returns "offline"
+ *
+ * @return the XML element name of the packet extension.
+ */
+ public String getElementName() {
+ return "offline";
+ }
+
+ /**
+ * Returns the XML namespace of the extension sub-packet root element.
+ * According the specification the namespace is always "http://jabber.org/protocol/offline"
+ *
+ * @return the XML namespace of the packet extension.
+ */
+ public String getNamespace() {
+ return "http://jabber.org/protocol/offline";
+ }
+
+ /**
+ * Returns the stamp that uniquely identifies the offline message. This stamp may
+ * be used for deleting the offline message. The stamp may be of the form UTC timestamps
+ * but it is not required to have that format.
+ *
+ * @return the stamp that uniquely identifies the offline message.
+ */
+ public String getNode() {
+ return node;
+ }
+
+ /**
+ * Sets the stamp that uniquely identifies the offline message. This stamp may
+ * be used for deleting the offline message. The stamp may be of the form UTC timestamps
+ * but it is not required to have that format.
+ *
+ * @param node the stamp that uniquely identifies the offline message.
+ */
+ public void setNode(String node) {
+ this.node = node;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+ "\">");
+ if (getNode() != null)
+ buf.append("<item node=\"").append(getNode()).append("\"/>");
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new Provider.
+ * ProviderManager requires that every PacketExtensionProvider has a public,
+ * no-argument constructor
+ */
+ public Provider() {
+ }
+
+ /**
+ * Parses a OfflineMessageInfo packet (extension sub-packet).
+ *
+ * @param parser the XML parser, positioned at the starting element of the extension.
+ * @return a PacketExtension.
+ * @throws Exception if a parsing error occurs.
+ */
+ public PacketExtension parseExtension(XmlPullParser parser)
+ throws Exception {
+ OfflineMessageInfo info = new OfflineMessageInfo();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item"))
+ info.setNode(parser.getAttributeValue("", "node"));
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("offline")) {
+ done = true;
+ }
+ }
+ }
+
+ return info;
+ }
+
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java
new file mode 100644
index 000000000..9b726e3c6
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java
@@ -0,0 +1,237 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Represents a request to get some or all the offline messages of a user. This class can also
+ * be used for deleting some or all the offline messages of a user.
+ *
+ * @author Gaston Dombiak
+ */
+public class OfflineMessageRequest extends IQ {
+
+ private List items = new ArrayList();
+ private boolean purge = false;
+ private boolean fetch = false;
+
+ /**
+ * Returns an Iterator for item childs that holds information about offline messages to
+ * view or delete.
+ *
+ * @return an Iterator for item childs that holds information about offline messages to
+ * view or delete.
+ */
+ public Iterator getItems() {
+ synchronized (items) {
+ return Collections.unmodifiableList(new ArrayList(items)).iterator();
+ }
+ }
+
+ /**
+ * Adds an item child that holds information about offline messages to view or delete.
+ *
+ * @param item the item child that holds information about offline messages to view or delete.
+ */
+ public void addItem(Item item) {
+ synchronized (items) {
+ items.add(item);
+ }
+ }
+
+ /**
+ * Returns true if all the offline messages of the user should be deleted.
+ *
+ * @return true if all the offline messages of the user should be deleted.
+ */
+ public boolean isPurge() {
+ return purge;
+ }
+
+ /**
+ * Sets if all the offline messages of the user should be deleted.
+ *
+ * @param purge true if all the offline messages of the user should be deleted.
+ */
+ public void setPurge(boolean purge) {
+ this.purge = purge;
+ }
+
+ /**
+ * Returns true if all the offline messages of the user should be retrieved.
+ *
+ * @return true if all the offline messages of the user should be retrieved.
+ */
+ public boolean isFetch() {
+ return fetch;
+ }
+
+ /**
+ * Sets if all the offline messages of the user should be retrieved.
+ *
+ * @param fetch true if all the offline messages of the user should be retrieved.
+ */
+ public void setFetch(boolean fetch) {
+ this.fetch = fetch;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<offline xmlns=\"http://jabber.org/protocol/offline\">");
+ synchronized (items) {
+ for (int i = 0; i < items.size(); i++) {
+ Item item = (Item) items.get(i);
+ buf.append(item.toXML());
+ }
+ }
+ if (purge) {
+ buf.append("<purge/>");
+ }
+ if (fetch) {
+ buf.append("<fetch/>");
+ }
+ // Add packet extensions, if any are defined.
+ buf.append(getExtensionsXML());
+ buf.append("</offline>");
+ return buf.toString();
+ }
+
+ /**
+ * Item child that holds information about offline messages to view or delete.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Item {
+ private String action;
+ private String jid;
+ private String node;
+
+ /**
+ * Creates a new item child.
+ *
+ * @param node the actor's affiliation to the room
+ */
+ public Item(String node) {
+ this.node = node;
+ }
+
+ public String getNode() {
+ return node;
+ }
+
+ /**
+ * Returns "view" or "remove" that indicate if the server should return the specified
+ * offline message or delete it.
+ *
+ * @return "view" or "remove" that indicate if the server should return the specified
+ * offline message or delete it.
+ */
+ public String getAction() {
+ return action;
+ }
+
+ /**
+ * Sets if the server should return the specified offline message or delete it. Possible
+ * values are "view" or "remove".
+ *
+ * @param action if the server should return the specified offline message or delete it.
+ */
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public String getJid() {
+ return jid;
+ }
+
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<item");
+ if (getAction() != null) {
+ buf.append(" action=\"").append(getAction()).append("\"");
+ }
+ if (getJid() != null) {
+ buf.append(" jid=\"").append(getJid()).append("\"");
+ }
+ if (getNode() != null) {
+ buf.append(" node=\"").append(getNode()).append("\"");
+ }
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+
+ public static class Provider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ OfflineMessageRequest request = new OfflineMessageRequest();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ request.addItem(parseItem(parser));
+ }
+ else if (parser.getName().equals("purge")) {
+ request.setPurge(true);
+ }
+ else if (parser.getName().equals("fetch")) {
+ request.setFetch(true);
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("offline")) {
+ done = true;
+ }
+ }
+ }
+
+ return request;
+ }
+
+ private Item parseItem(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ Item item = new Item(parser.getAttributeValue("", "node"));
+ item.setAction(parser.getAttributeValue("", "action"));
+ item.setJid(parser.getAttributeValue("", "jid"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ done = true;
+ }
+ }
+ }
+ return item;
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/PrivateData.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/PrivateData.java
new file mode 100644
index 000000000..c83269c51
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/PrivateData.java
@@ -0,0 +1,52 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+/**
+ * Interface to represent private data. Each private data chunk is an XML sub-document
+ * with a root element name and namespace.
+ *
+ * @see org.jivesoftware.smackx.PrivateDataManager
+ * @author Matt Tucker
+ */
+public interface PrivateData {
+
+ /**
+ * Returns the root element name.
+ *
+ * @return the element name.
+ */
+ public String getElementName();
+
+ /**
+ * Returns the root element XML namespace.
+ *
+ * @return the namespace.
+ */
+ public String getNamespace();
+
+ /**
+ * Returns the XML reppresentation of the PrivateData.
+ *
+ * @return the private data as XML.
+ */
+ public String toXML();
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/RosterExchange.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/RosterExchange.java
new file mode 100644
index 000000000..553c6dcdf
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/RosterExchange.java
@@ -0,0 +1,175 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.util.*;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.*;
+
+/**
+ * Represents XMPP Roster Item Exchange packets.<p>
+ *
+ * The 'jabber:x:roster' namespace (which is not to be confused with the 'jabber:iq:roster'
+ * namespace) is used to send roster items from one client to another. A roster item is sent by
+ * adding to the &lt;message/&gt; element an &lt;x/&gt; child scoped by the 'jabber:x:roster' namespace. This
+ * &lt;x/&gt; element may contain one or more &lt;item/&gt; children (one for each roster item to be sent).<p>
+ *
+ * Each &lt;item/&gt; element may possess the following attributes:<p>
+ *
+ * &lt;jid/&gt; -- The id of the contact being sent. This attribute is required.<br>
+ * &lt;name/&gt; -- A natural-language nickname for the contact. This attribute is optional.<p>
+ *
+ * Each &lt;item/&gt; element may also contain one or more &lt;group/&gt; children specifying the
+ * natural-language name of a user-specified group, for the purpose of categorizing this contact
+ * into one or more roster groups.
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchange implements PacketExtension {
+
+ private List remoteRosterEntries = new ArrayList();
+
+ /**
+ * Creates a new empty roster exchange package.
+ *
+ */
+ public RosterExchange() {
+ super();
+ }
+
+ /**
+ * Creates a new roster exchange package with the entries specified in roster.
+ *
+ * @param roster the roster to send to other XMPP entity.
+ */
+ public RosterExchange(Roster roster) {
+ // Add all the roster entries to the new RosterExchange
+ for (Iterator rosterEntries = roster.getEntries(); rosterEntries.hasNext();) {
+ this.addRosterEntry((RosterEntry) rosterEntries.next());
+ }
+ }
+
+ /**
+ * Adds a roster entry to the packet.
+ *
+ * @param rosterEntry a roster entry to add.
+ */
+ public void addRosterEntry(RosterEntry rosterEntry) {
+ // Obtain a String[] from the roster entry groups name
+ ArrayList groupNamesList = new ArrayList();
+ String[] groupNames;
+ for (Iterator groups = rosterEntry.getGroups(); groups.hasNext();) {
+ groupNamesList.add(((RosterGroup) groups.next()).getName());
+ }
+ groupNames = (String[]) groupNamesList.toArray(new String[groupNamesList.size()]);
+
+ // Create a new Entry based on the rosterEntry and add it to the packet
+ RemoteRosterEntry remoteRosterEntry = new RemoteRosterEntry(rosterEntry.getUser(), rosterEntry.getName(), groupNames);
+
+ addRosterEntry(remoteRosterEntry);
+ }
+
+ /**
+ * Adds a remote roster entry to the packet.
+ *
+ * @param remoteRosterEntry a remote roster entry to add.
+ */
+ public void addRosterEntry(RemoteRosterEntry remoteRosterEntry) {
+ synchronized (remoteRosterEntries) {
+ remoteRosterEntries.add(remoteRosterEntry);
+ }
+ }
+
+ /**
+ * Returns the XML element name of the extension sub-packet root element.
+ * Always returns "x"
+ *
+ * @return the XML element name of the packet extension.
+ */
+ public String getElementName() {
+ return "x";
+ }
+
+ /**
+ * Returns the XML namespace of the extension sub-packet root element.
+ * According the specification the namespace is always "jabber:x:roster"
+ * (which is not to be confused with the 'jabber:iq:roster' namespace
+ *
+ * @return the XML namespace of the packet extension.
+ */
+ public String getNamespace() {
+ return "jabber:x:roster";
+ }
+
+ /**
+ * Returns an Iterator for the roster entries in the packet.
+ *
+ * @return an Iterator for the roster entries in the packet.
+ */
+ public Iterator getRosterEntries() {
+ synchronized (remoteRosterEntries) {
+ List entries = Collections.unmodifiableList(new ArrayList(remoteRosterEntries));
+ return entries.iterator();
+ }
+ }
+
+ /**
+ * Returns a count of the entries in the roster exchange.
+ *
+ * @return the number of entries in the roster exchange.
+ */
+ public int getEntryCount() {
+ return remoteRosterEntries.size();
+ }
+
+ /**
+ * Returns the XML representation of a Roster Item Exchange according the specification.
+ *
+ * Usually the XML representation will be inside of a Message XML representation like
+ * in the following example:
+ * <pre>
+ * &lt;message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"&gt;
+ * &lt;subject&gt;Any subject you want&lt;/subject&gt;
+ * &lt;body&gt;This message contains roster items.&lt;/body&gt;
+ * &lt;x xmlns="jabber:x:roster"&gt;
+ * &lt;item jid="gato1@gato.home"/&gt;
+ * &lt;item jid="gato2@gato.home"/&gt;
+ * &lt;/x&gt;
+ * &lt;/message&gt;
+ * </pre>
+ *
+ */
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+ "\">");
+ // Loop through all roster entries and append them to the string buffer
+ for (Iterator i = getRosterEntries(); i.hasNext();) {
+ RemoteRosterEntry remoteRosterEntry = (RemoteRosterEntry) i.next();
+ buf.append(remoteRosterEntry.toXML());
+ }
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java
new file mode 100644
index 000000000..b572a7ae6
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java
@@ -0,0 +1,73 @@
+package org.jivesoftware.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * IQ packet used for discovering the user's shared groups and for getting the answer back
+ * from the server.<p>
+ *
+ * Important note: This functionality is not part of the XMPP spec and it will only work
+ * with Wildfire.
+ *
+ * @author Gaston Dombiak
+ */
+public class SharedGroupsInfo extends IQ {
+
+ private List groups = new ArrayList();
+
+ /**
+ * Returns a collection with the shared group names returned from the server.
+ *
+ * @return collection with the shared group names returned from the server.
+ */
+ public List getGroups() {
+ return groups;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<sharedgroup xmlns=\"http://www.jivesoftware.org/protocol/sharedgroup\">");
+ for (Iterator it=groups.iterator(); it.hasNext();) {
+ buf.append("<group>").append(it.next()).append("</group>");
+ }
+ buf.append("</sharedgroup>");
+ return buf.toString();
+ }
+
+ /**
+ * Internal Search service Provider.
+ */
+ public static class Provider implements IQProvider {
+
+ /**
+ * Provider Constructor.
+ */
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ SharedGroupsInfo groupsInfo = new SharedGroupsInfo();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG && parser.getName().equals("group")) {
+ groupsInfo.getGroups().add(parser.nextText());
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("sharedgroup")) {
+ done = true;
+ }
+ }
+ }
+ return groupsInfo;
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/StreamInitiation.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/StreamInitiation.java
new file mode 100644
index 000000000..75f44814c
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/StreamInitiation.java
@@ -0,0 +1,419 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.Date;
+
+/**
+ * The process by which two entities initiate a stream.
+ *
+ * @author Alexander Wenckus
+ */
+public class StreamInitiation extends IQ {
+
+ private String id;
+
+ private String mimeType;
+
+ private File file;
+
+ private Feature featureNegotiation;
+
+ /**
+ * The "id" attribute is an opaque identifier. This attribute MUST be
+ * present on type='set', and MUST be a valid string. This SHOULD NOT be
+ * sent back on type='result', since the <iq/> "id" attribute provides the
+ * only context needed. This value is generated by the Sender, and the same
+ * value MUST be used throughout a session when talking to the Receiver.
+ *
+ * @param id The "id" attribute.
+ */
+ public void setSesssionID(final String id) {
+ this.id = id;
+ }
+
+ /**
+ * Uniquely identifies a stream initiation to the recipient.
+ *
+ * @return The "id" attribute.
+ * @see #setSesssionID(String)
+ */
+ public String getSessionID() {
+ return id;
+ }
+
+ /**
+ * The "mime-type" attribute identifies the MIME-type for the data across
+ * the stream. This attribute MUST be a valid MIME-type as registered with
+ * the Internet Assigned Numbers Authority (IANA) [3] (specifically, as
+ * listed at <http://www.iana.org/assignments/media-types>). During
+ * negotiation, this attribute SHOULD be present, and is otherwise not
+ * required. If not included during negotiation, its value is assumed to be
+ * "binary/octect-stream".
+ *
+ * @param mimeType The valid mime-type.
+ */
+ public void setMimeType(final String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ /**
+ * Identifies the type of file that is desired to be transfered.
+ *
+ * @return The mime-type.
+ * @see #setMimeType(String)
+ */
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ /**
+ * Sets the file which contains the information pertaining to the file to be
+ * transfered.
+ *
+ * @param file The file identified by the stream initiator to be sent.
+ */
+ public void setFile(final File file) {
+ this.file = file;
+ }
+
+ /**
+ * Returns the file containing the information about the request.
+ *
+ * @return Returns the file containing the information about the request.
+ */
+ public File getFile() {
+ return file;
+ }
+
+ /**
+ * Sets the data form which contains the valid methods of stream neotiation
+ * and transfer.
+ *
+ * @param form The dataform containing the methods.
+ */
+ public void setFeatureNegotiationForm(final DataForm form) {
+ this.featureNegotiation = new Feature(form);
+ }
+
+ /**
+ * Returns the data form which contains the valid methods of stream
+ * neotiation and transfer.
+ *
+ * @return Returns the data form which contains the valid methods of stream
+ * neotiation and transfer.
+ */
+ public DataForm getFeatureNegotiationForm() {
+ return featureNegotiation.getData();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.packet.IQ#getChildElementXML()
+ */
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ if (this.getType().equals(IQ.Type.SET)) {
+ buf.append("<si xmlns=\"http://jabber.org/protocol/si\" ");
+ if (getSessionID() != null) {
+ buf.append("id=\"").append(getSessionID()).append("\" ");
+ }
+ if (getMimeType() != null) {
+ buf.append("mime-type=\"").append(getMimeType()).append("\" ");
+ }
+ buf
+ .append("profile=\"http://jabber.org/protocol/si/profile/file-transfer\">");
+
+ // Add the file section if there is one.
+ String fileXML = file.toXML();
+ if (fileXML != null) {
+ buf.append(fileXML);
+ }
+ }
+ else if (this.getType().equals(IQ.Type.RESULT)) {
+ buf.append("<si xmlns=\"http://jabber.org/protocol/si\">");
+ }
+ else {
+ throw new IllegalArgumentException("IQ Type not understood");
+ }
+ if (featureNegotiation != null) {
+ buf.append(featureNegotiation.toXML());
+ }
+ buf.append("</si>");
+ return buf.toString();
+ }
+
+ /**
+ * <ul>
+ * <li>size: The size, in bytes, of the data to be sent.</li>
+ * <li>name: The name of the file that the Sender wishes to send.</li>
+ * <li>date: The last modification time of the file. This is specified
+ * using the DateTime profile as described in Jabber Date and Time Profiles.</li>
+ * <li>hash: The MD5 sum of the file contents.</li>
+ * </ul>
+ * <p/>
+ * <p/>
+ * &lt;desc&gt; is used to provide a sender-generated description of the
+ * file so the receiver can better understand what is being sent. It MUST
+ * NOT be sent in the result.
+ * <p/>
+ * <p/>
+ * When &lt;range&gt; is sent in the offer, it should have no attributes.
+ * This signifies that the sender can do ranged transfers. When a Stream
+ * Initiation result is sent with the <range> element, it uses these
+ * attributes:
+ * <p/>
+ * <ul>
+ * <li>offset: Specifies the position, in bytes, to start transferring the
+ * file data from. This defaults to zero (0) if not specified.</li>
+ * <li>length - Specifies the number of bytes to retrieve starting at
+ * offset. This defaults to the length of the file from offset to the end.</li>
+ * </ul>
+ * <p/>
+ * <p/>
+ * Both attributes are OPTIONAL on the &lt;range&gt; element. Sending no
+ * attributes is synonymous with not sending the &lt;range&gt; element. When
+ * no &lt;range&gt; element is sent in the Stream Initiation result, the
+ * Sender MUST send the complete file starting at offset 0. More generally,
+ * data is sent over the stream byte for byte starting at the offset
+ * position for the length specified.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class File implements PacketExtension {
+
+ private final String name;
+
+ private final long size;
+
+ private String hash;
+
+ private Date date;
+
+ private String desc;
+
+ private boolean isRanged;
+
+ /**
+ * Constructor providing the name of the file and its size.
+ *
+ * @param name The name of the file.
+ * @param size The size of the file in bytes.
+ */
+ public File(final String name, final long size) {
+ if (name == null) {
+ throw new NullPointerException("name cannot be null");
+ }
+
+ this.name = name;
+ this.size = size;
+ }
+
+ /**
+ * Returns the file's name.
+ *
+ * @return Returns the file's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the file's size.
+ *
+ * @return Returns the file's size.
+ */
+ public long getSize() {
+ return size;
+ }
+
+ /**
+ * Sets the MD5 sum of the file's contents
+ *
+ * @param hash The MD5 sum of the file's contents.
+ */
+ public void setHash(final String hash) {
+ this.hash = hash;
+ }
+
+ /**
+ * Returns the MD5 sum of the file's contents
+ *
+ * @return Returns the MD5 sum of the file's contents
+ */
+ public String getHash() {
+ return hash;
+ }
+
+ /**
+ * Sets the date that the file was last modified.
+ *
+ * @param date The date that the file was last modified.
+ */
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ /**
+ * Returns the date that the file was last modified.
+ *
+ * @return Returns the date that the file was last modified.
+ */
+ public Date getDate() {
+ return date;
+ }
+
+ /**
+ * Sets the description of the file.
+ *
+ * @param desc The description of the file so that the file reciever can
+ * know what file it is.
+ */
+ public void setDesc(final String desc) {
+ this.desc = desc;
+ }
+
+ /**
+ * Returns the description of the file.
+ *
+ * @return Returns the description of the file.
+ */
+ public String getDesc() {
+ return desc;
+ }
+
+ /**
+ * True if a range can be provided and false if it cannot.
+ *
+ * @param isRanged True if a range can be provided and false if it cannot.
+ */
+ public void setRanged(final boolean isRanged) {
+ this.isRanged = isRanged;
+ }
+
+ /**
+ * Returns whether or not the initiator can support a range for the file
+ * tranfer.
+ *
+ * @return Returns whether or not the initiator can support a range for
+ * the file tranfer.
+ */
+ public boolean isRanged() {
+ return isRanged;
+ }
+
+ public String getElementName() {
+ return "file";
+ }
+
+ public String getNamespace() {
+ return "http://jabber.org/protocol/si/profile/file-transfer";
+ }
+
+ public String toXML() {
+ StringBuffer buffer = new StringBuffer();
+
+ buffer.append("<").append(getElementName()).append(" xmlns=\"")
+ .append(getNamespace()).append("\" ");
+
+ if (getName() != null) {
+ buffer.append("name=\"").append(getName()).append("\" ");
+ }
+
+ if (getSize() > 0) {
+ buffer.append("size=\"").append(getSize()).append("\" ");
+ }
+
+ if (getDate() != null) {
+ buffer.append("date=\"").append(DelayInformation.UTC_FORMAT.format(date)).append("\" ");
+ }
+
+ if (getHash() != null) {
+ buffer.append("hash=\"").append(getHash()).append("\" ");
+ }
+
+ if ((desc != null && desc.length() > 0) || isRanged) {
+ buffer.append(">");
+ if (getDesc() != null && desc.length() > 0) {
+ buffer.append("<desc>").append(StringUtils.escapeForXML(getDesc())).append("</desc>");
+ }
+ if (isRanged()) {
+ buffer.append("<range/>");
+ }
+ buffer.append("</").append(getElementName()).append(">");
+ }
+ else {
+ buffer.append("/>");
+ }
+ return buffer.toString();
+ }
+ }
+
+ /**
+ * The feature negotiation portion of the StreamInitiation packet.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public class Feature implements PacketExtension {
+
+ private final DataForm data;
+
+ /**
+ * The dataform can be provided as part of the constructor.
+ *
+ * @param data The dataform.
+ */
+ public Feature(final DataForm data) {
+ this.data = data;
+ }
+
+ /**
+ * Returns the dataform associated with the feature negotiation.
+ *
+ * @return Returns the dataform associated with the feature negotiation.
+ */
+ public DataForm getData() {
+ return data;
+ }
+
+ public String getNamespace() {
+ return "http://jabber.org/protocol/feature-neg";
+ }
+
+ public String getElementName() {
+ return "feature";
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf
+ .append("<feature xmlns=\"http://jabber.org/protocol/feature-neg\">");
+ buf.append(data.toXML());
+ buf.append("</feature>");
+ return buf.toString();
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Time.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Time.java
new file mode 100644
index 000000000..9ceea620f
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Time.java
@@ -0,0 +1,196 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+import java.util.*;
+import java.text.SimpleDateFormat;
+import java.text.DateFormat;
+
+/**
+ * A Time IQ packet, which is used by XMPP clients to exchange their respective local
+ * times. Clients that wish to fully support the entitity time protocol should register
+ * a PacketListener for incoming time requests that then respond with the local time.
+ * This class can be used to request the time from other clients, such as in the
+ * following code snippet:
+ *
+ * <pre>
+ * // Request the time from a remote user.
+ * Time timeRequest = new Time();
+ * timeRequest.setType(IQ.Type.GET);
+ * timeRequest.setTo(someUser@example.com);
+ *
+ * // Create a packet collector to listen for a response.
+ * PacketCollector collector = con.createPacketCollector(
+ * new PacketIDFilter(timeRequest.getPacketID()));
+ *
+ * con.sendPacket(timeRequest);
+ *
+ * // Wait up to 5 seconds for a result.
+ * IQ result = (IQ)collector.nextResult(5000);
+ * if (result != null && result.getType() == IQ.Type.RESULT) {
+ * Time timeResult = (Time)result;
+ * // Do something with result...
+ * }</pre><p>
+ *
+ * Warning: this is an non-standard protocol documented by
+ * <a href="http://www.jabber.org/jeps/jep-0090.html">JEP-90</a>. Because this is a
+ * non-standard protocol, it is subject to change.
+ *
+ * @author Matt Tucker
+ */
+public class Time extends IQ {
+
+ private static SimpleDateFormat utcFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+ private static DateFormat displayFormat = DateFormat.getDateTimeInstance();
+
+ private String utc = null;
+ private String tz = null;
+ private String display = null;
+
+ /**
+ * Creates a new Time instance with empty values for all fields.
+ */
+ public Time() {
+ this(Calendar.getInstance());
+ }
+
+ /**
+ * Creates a new Time instance using the specified calendar instance as
+ * the time value to send.
+ *
+ * @param cal the time value.
+ */
+ public Time(Calendar cal) {
+ TimeZone timeZone = cal.getTimeZone();
+ tz = cal.getTimeZone().getID();
+ display = displayFormat.format(cal.getTime());
+ // Convert local time to the UTC time.
+ utc = utcFormat.format(new Date(
+ cal.getTimeInMillis() - timeZone.getOffset(cal.getTimeInMillis())));
+ }
+
+ /**
+ * Returns the local time or <tt>null</tt> if the time hasn't been set.
+ *
+ * @return the lcocal time.
+ */
+ public Date getTime() {
+ if (utc == null) {
+ return null;
+ }
+ Date date = null;
+ try {
+ Calendar cal = Calendar.getInstance();
+ // Convert the UTC time to local time.
+ cal.setTime(new Date(utcFormat.parse(utc).getTime() +
+ cal.getTimeZone().getOffset(cal.getTimeInMillis())));
+ date = cal.getTime();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ return date;
+ }
+
+ /**
+ * Sets the time using the local time.
+ *
+ * @param time the current local time.
+ */
+ public void setTime(Date time) {
+ // Convert local time to UTC time.
+ utc = utcFormat.format(new Date(
+ time.getTime() - TimeZone.getDefault().getOffset(time.getTime())));
+ }
+
+ /**
+ * Returns the time as a UTC formatted String using the format CCYYMMDDThh:mm:ss.
+ *
+ * @return the time as a UTC formatted String.
+ */
+ public String getUtc() {
+ return utc;
+ }
+
+ /**
+ * Sets the time using UTC formatted String in the format CCYYMMDDThh:mm:ss.
+ *
+ * @param utc the time using a formatted String.
+ */
+ public void setUtc(String utc) {
+ this.utc = utc;
+
+ }
+
+ /**
+ * Returns the time zone.
+ *
+ * @return the time zone.
+ */
+ public String getTz() {
+ return tz;
+ }
+
+ /**
+ * Sets the time zone.
+ *
+ * @param tz the time zone.
+ */
+ public void setTz(String tz) {
+ this.tz = tz;
+ }
+
+ /**
+ * Returns the local (non-utc) time in human-friendly format.
+ *
+ * @return the local time in human-friendly format.
+ */
+ public String getDisplay() {
+ return display;
+ }
+
+ /**
+ * Sets the local time in human-friendly format.
+ *
+ * @param display the local time in human-friendly format.
+ */
+ public void setDisplay(String display) {
+ this.display = display;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:time\">");
+ if (utc != null) {
+ buf.append("<utc>").append(utc).append("</utc>");
+ }
+ if (tz != null) {
+ buf.append("<tz>").append(tz).append("</tz>");
+ }
+ if (display != null) {
+ buf.append("<display>").append(display).append("</display>");
+ }
+ buf.append("</query>");
+ return buf.toString();
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/VCard.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/VCard.java
new file mode 100644
index 000000000..4f1248859
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/VCard.java
@@ -0,0 +1,798 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A VCard class for use with the
+ * <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p>
+ * <p/>
+ * You should refer to the
+ * <a href="http://www.jabber.org/jeps/jep-0054.html" target="_blank">JEP-54 documentation</a>.<p>
+ * <p/>
+ * Please note that this class is incomplete but it does provide the most commonly found
+ * information in vCards. Also remember that VCard transfer is not a standard, and the protocol
+ * may change or be replaced.<p>
+ * <p/>
+ * <b>Usage:</b>
+ * <pre>
+ * <p/>
+ * // To save VCard:
+ * <p/>
+ * VCard vCard = new VCard();
+ * vCard.setFirstName("kir");
+ * vCard.setLastName("max");
+ * vCard.setEmailHome("foo@fee.bar");
+ * vCard.setJabberId("jabber@id.org");
+ * vCard.setOrganization("Jetbrains, s.r.o");
+ * vCard.setNickName("KIR");
+ * <p/>
+ * vCard.setField("TITLE", "Mr");
+ * vCard.setAddressFieldHome("STREET", "Some street");
+ * vCard.setAddressFieldWork("CTRY", "US");
+ * vCard.setPhoneWork("FAX", "3443233");
+ * <p/>
+ * vCard.save(connection);
+ * <p/>
+ * // To load VCard:
+ * <p/>
+ * VCard vCard = new VCard();
+ * vCard.load(conn); // load own VCard
+ * vCard.load(conn, "joe@foo.bar"); // load someone's VCard
+ * </pre>
+ *
+ * @author Kirill Maximov (kir@maxkir.com)
+ */
+public class VCard extends IQ {
+
+ /**
+ * Phone types:
+ * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF?
+ */
+ private Map homePhones = new HashMap();
+ private Map workPhones = new HashMap();
+
+
+ /**
+ * Address types:
+ * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?,
+ * REGION?, PCODE?, CTRY?
+ */
+ private Map homeAddr = new HashMap();
+ private Map workAddr = new HashMap();
+
+ private String firstName;
+ private String lastName;
+ private String middleName;
+
+ private String emailHome;
+ private String emailWork;
+
+ private String organization;
+ private String organizationUnit;
+
+ private String avatar;
+
+ /**
+ * Such as DESC ROLE GEO etc.. see JEP-0054
+ */
+ private Map otherSimpleFields = new HashMap();
+
+ public VCard() {
+ }
+
+ /**
+ * Set generic VCard field.
+ *
+ * @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ,
+ * GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC.
+ */
+ public String getField(String field) {
+ return (String) otherSimpleFields.get(field);
+ }
+
+ /**
+ * Set generic VCard field.
+ *
+ * @param value value of field
+ * @param field field to set. See {@link #getField(String)}
+ * @see #getField(String)
+ */
+ public void setField(String field, String value) {
+ otherSimpleFields.put(field, value);
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getMiddleName() {
+ return middleName;
+ }
+
+ public void setMiddleName(String middleName) {
+ this.middleName = middleName;
+ }
+
+ public String getNickName() {
+ return (String) otherSimpleFields.get("NICKNAME");
+ }
+
+ public void setNickName(String nickName) {
+ otherSimpleFields.put("NICKNAME", nickName);
+ }
+
+ public String getEmailHome() {
+ return emailHome;
+ }
+
+ public void setEmailHome(String email) {
+ this.emailHome = email;
+ }
+
+ public String getEmailWork() {
+ return emailWork;
+ }
+
+ public void setEmailWork(String emailWork) {
+ this.emailWork = emailWork;
+ }
+
+ public String getJabberId() {
+ return (String) otherSimpleFields.get("JABBERID");
+ }
+
+ public void setJabberId(String jabberId) {
+ otherSimpleFields.put("JABBERID", jabberId);
+ }
+
+ public String getOrganization() {
+ return organization;
+ }
+
+ public void setOrganization(String organization) {
+ this.organization = organization;
+ }
+
+ public String getOrganizationUnit() {
+ return organizationUnit;
+ }
+
+ public void setOrganizationUnit(String organizationUnit) {
+ this.organizationUnit = organizationUnit;
+ }
+
+ /**
+ * Get home address field
+ *
+ * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
+ * LOCALITY, REGION, PCODE, CTRY
+ */
+ public String getAddressFieldHome(String addrField) {
+ return (String) homeAddr.get(addrField);
+ }
+
+ /**
+ * Set home address field
+ *
+ * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
+ * LOCALITY, REGION, PCODE, CTRY
+ */
+ public void setAddressFieldHome(String addrField, String value) {
+ homeAddr.put(addrField, value);
+ }
+
+ /**
+ * Get work address field
+ *
+ * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
+ * LOCALITY, REGION, PCODE, CTRY
+ */
+ public String getAddressFieldWork(String addrField) {
+ return (String) workAddr.get(addrField);
+ }
+
+ /**
+ * Set work address field
+ *
+ * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
+ * LOCALITY, REGION, PCODE, CTRY
+ */
+ public void setAddressFieldWork(String addrField, String value) {
+ workAddr.put(addrField, value);
+ }
+
+
+ /**
+ * Set home phone number
+ *
+ * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
+ * @param phoneNum phone number
+ */
+ public void setPhoneHome(String phoneType, String phoneNum) {
+ homePhones.put(phoneType, phoneNum);
+ }
+
+ /**
+ * Get home phone number
+ *
+ * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
+ */
+ public String getPhoneHome(String phoneType) {
+ return (String) homePhones.get(phoneType);
+ }
+
+ /**
+ * Set work phone number
+ *
+ * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
+ * @param phoneNum phone number
+ */
+ public void setPhoneWork(String phoneType, String phoneNum) {
+ workPhones.put(phoneType, phoneNum);
+ }
+
+ /**
+ * Get work phone number
+ *
+ * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
+ */
+ public String getPhoneWork(String phoneType) {
+ return (String) workPhones.get(phoneType);
+ }
+
+ /**
+ * Set the avatar for the VCard by specifying the url to the image.
+ *
+ * @param avatarURL the url to the image(png,jpeg,gif,bmp)
+ */
+ public void setAvatar(URL avatarURL) {
+ byte[] bytes = new byte[0];
+ try {
+ bytes = getBytes(avatarURL);
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ String encodedImage = StringUtils.encodeBase64(bytes);
+ avatar = encodedImage;
+
+ setField("PHOTO", "<TYPE>image/jpeg</TYPE><BINVAL>" + encodedImage + "</BINVAL>");
+ }
+
+ /**
+ * Specify the bytes for the avatar to use.
+ *
+ * @param bytes the bytes of the avatar.
+ */
+ public void setAvatar(byte[] bytes) {
+ String encodedImage = StringUtils.encodeBase64(bytes);
+ avatar = encodedImage;
+
+ setField("PHOTO", "<TYPE>image/jpeg</TYPE><BINVAL>" + encodedImage + "</BINVAL>");
+ }
+
+ /**
+ * Set the encoded avatar string. This is used by the provider.
+ *
+ * @param encodedAvatar the encoded avatar string.
+ */
+ public void setEncodedImage(String encodedAvatar) {
+ //TODO Move VCard and VCardProvider into a vCard package.
+ this.avatar = encodedAvatar;
+ }
+
+ /**
+ * Return the byte representation of the avatar(if one exists), otherwise returns null if
+ * no avatar could be found.
+ * <b>Example 1</b>
+ * <pre>
+ * // Load Avatar from VCard
+ * byte[] avatarBytes = vCard.getAvatar();
+ * <p/>
+ * // To create an ImageIcon for Swing applications
+ * ImageIcon icon = new ImageIcon(avatar);
+ * <p/>
+ * // To create just an image object from the bytes
+ * ByteArrayInputStream bais = new ByteArrayInputStream(avatar);
+ * try {
+ * Image image = ImageIO.read(bais);
+ * }
+ * catch (IOException e) {
+ * e.printStackTrace();
+ * }
+ * </pre>
+ *
+ * @return byte representation of avatar.
+ */
+ public byte[] getAvatar() {
+ if (avatar == null) {
+ return null;
+ }
+ if (avatar != null) {
+ return StringUtils.decodeBase64(avatar);
+ }
+ return null;
+ }
+
+ /**
+ * Common code for getting the bytes of a url.
+ *
+ * @param url the url to read.
+ */
+ public static byte[] getBytes(URL url) throws IOException {
+ final String path = url.getPath();
+ final File file = new File(path);
+ if (file.exists()) {
+ return getFileBytes(file);
+ }
+
+ return null;
+ }
+
+ private static byte[] getFileBytes(File file) throws IOException {
+ BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+ int bytes = (int) file.length();
+ byte[] buffer = new byte[bytes];
+ int readBytes = bis.read(buffer);
+ bis.close();
+ return buffer;
+ }
+
+ /**
+ * Returns the SHA-1 Hash of the Avatar image.
+ *
+ * @return the SHA-1 Hash of the Avatar image.
+ */
+ public String getAvatarHash() {
+ byte[] bytes = getAvatar();
+ if (bytes == null) {
+ return null;
+ }
+
+ MessageDigest digest = null;
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ }
+ catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+
+ digest.update(bytes);
+ return StringUtils.encodeHex(digest.digest());
+ }
+
+ /**
+ * Save this vCard for the user connected by 'connection'. Connection should be authenticated
+ * and not anonymous.<p>
+ * <p/>
+ * NOTE: the method is asynchronous and does not wait for the returned value.
+ * @param connection the XMPPConnection to use.
+ * @throws XMPPException thrown if there was an issue setting the VCard in the server.
+ */
+ public void save(XMPPConnection connection) throws XMPPException {
+ checkAuthenticated(connection);
+
+ setType(IQ.Type.SET);
+ setFrom(connection.getUser());
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(getPacketID()));
+ connection.sendPacket(this);
+
+ Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ }
+
+ /**
+ * Load VCard information for a connected user. Connection should be authenticated
+ * and not anonymous.
+ */
+ public void load(XMPPConnection connection) throws XMPPException {
+ checkAuthenticated(connection);
+
+ setFrom(connection.getUser());
+ doLoad(connection, connection.getUser());
+ }
+
+ /**
+ * Load VCard information for a given user. Connection should be authenticated and not anonymous.
+ */
+ public void load(XMPPConnection connection, String user) throws XMPPException {
+ checkAuthenticated(connection);
+
+ setTo(user);
+ doLoad(connection, user);
+ }
+
+ private void doLoad(XMPPConnection connection, String user) throws XMPPException {
+ setType(Type.GET);
+ PacketCollector collector = connection.createPacketCollector(
+ new PacketIDFilter(getPacketID()));
+ connection.sendPacket(this);
+
+ VCard result = null;
+ try {
+ result = (VCard) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ if (result == null) {
+ throw new XMPPException(new XMPPError(408, "Timeout getting VCard information"));
+ }
+ if (result.getError() != null) {
+ throw new XMPPException(result.getError());
+ }
+ }
+ catch (ClassCastException e) {
+ System.out.println("No VCard for " + user);
+ }
+
+ copyFieldsFrom(result);
+ }
+
+ public String getChildElementXML() {
+ StringBuffer sb = new StringBuffer();
+ new VCardWriter(sb).write();
+ return sb.toString();
+ }
+
+ private void copyFieldsFrom(VCard result) {
+ if (result == null) result = new VCard();
+
+ Field[] fields = VCard.class.getDeclaredFields();
+ for (int i = 0; i < fields.length; i++) {
+ Field field = fields[i];
+ if (field.getDeclaringClass() == VCard.class &&
+ !Modifier.isFinal(field.getModifiers())) {
+ try {
+ field.setAccessible(true);
+ field.set(this, field.get(result));
+ }
+ catch (IllegalAccessException e) {
+ throw new RuntimeException("This cannot happen:" + field, e);
+ }
+ }
+ }
+ }
+
+ private void checkAuthenticated(XMPPConnection connection) {
+ if (connection == null) {
+ new IllegalArgumentException("No connection was provided");
+ }
+ if (!connection.isAuthenticated()) {
+ new IllegalArgumentException("Connection is not authenticated");
+ }
+ if (connection.isAnonymous()) {
+ new IllegalArgumentException("Connection cannot be anonymous");
+ }
+ }
+
+ private boolean hasContent() {
+ //noinspection OverlyComplexBooleanExpression
+ return hasNameField()
+ || hasOrganizationFields()
+ || emailHome != null
+ || emailWork != null
+ || otherSimpleFields.size() > 0
+ || homeAddr.size() > 0
+ || homePhones.size() > 0
+ || workAddr.size() > 0
+ || workPhones.size() > 0
+ ;
+ }
+
+ private boolean hasNameField() {
+ return firstName != null || lastName != null || middleName != null;
+ }
+
+ private boolean hasOrganizationFields() {
+ return organization != null || organizationUnit != null;
+ }
+
+ // Used in tests:
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final VCard vCard = (VCard) o;
+
+ if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) {
+ return false;
+ }
+ if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) {
+ return false;
+ }
+ if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) {
+ return false;
+ }
+ if (!homeAddr.equals(vCard.homeAddr)) {
+ return false;
+ }
+ if (!homePhones.equals(vCard.homePhones)) {
+ return false;
+ }
+ if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) {
+ return false;
+ }
+ if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) {
+ return false;
+ }
+ if (organization != null ?
+ !organization.equals(vCard.organization) : vCard.organization != null) {
+ return false;
+ }
+ if (organizationUnit != null ?
+ !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) {
+ return false;
+ }
+ if (!otherSimpleFields.equals(vCard.otherSimpleFields)) {
+ return false;
+ }
+ if (!workAddr.equals(vCard.workAddr)) {
+ return false;
+ }
+ if (!workPhones.equals(vCard.workPhones)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public int hashCode() {
+ int result;
+ result = homePhones.hashCode();
+ result = 29 * result + workPhones.hashCode();
+ result = 29 * result + homeAddr.hashCode();
+ result = 29 * result + workAddr.hashCode();
+ result = 29 * result + (firstName != null ? firstName.hashCode() : 0);
+ result = 29 * result + (lastName != null ? lastName.hashCode() : 0);
+ result = 29 * result + (middleName != null ? middleName.hashCode() : 0);
+ result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0);
+ result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0);
+ result = 29 * result + (organization != null ? organization.hashCode() : 0);
+ result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0);
+ result = 29 * result + otherSimpleFields.hashCode();
+ return result;
+ }
+
+ public String toString() {
+ return getChildElementXML();
+ }
+
+ //==============================================================
+
+ private class VCardWriter {
+
+ private final StringBuffer sb;
+
+ VCardWriter(StringBuffer sb) {
+ this.sb = sb;
+ }
+
+ public void write() {
+ appendTag("vCard", "xmlns", "vcard-temp", hasContent(), new ContentBuilder() {
+ public void addTagContent() {
+ buildActualContent();
+ }
+ });
+ }
+
+ private void buildActualContent() {
+ if (hasNameField()) {
+ appendFN();
+ appendN();
+ }
+
+ appendOrganization();
+ appendGenericFields();
+
+ appendEmail(emailWork, "WORK");
+ appendEmail(emailHome, "HOME");
+
+ appendPhones(workPhones, "WORK");
+ appendPhones(homePhones, "HOME");
+
+ appendAddress(workAddr, "WORK");
+ appendAddress(homeAddr, "HOME");
+ }
+
+ private void appendEmail(final String email, final String type) {
+ if (email != null) {
+ appendTag("EMAIL", true, new ContentBuilder() {
+ public void addTagContent() {
+ appendEmptyTag(type);
+ appendEmptyTag("INTERNET");
+ appendEmptyTag("PREF");
+ appendTag("USERID", email);
+ }
+ });
+ }
+ }
+
+ private void appendPhones(Map phones, final String code) {
+ Iterator it = phones.entrySet().iterator();
+ while (it.hasNext()) {
+ final Map.Entry entry = (Map.Entry) it.next();
+ appendTag("TEL", true, new ContentBuilder() {
+ public void addTagContent() {
+ appendEmptyTag(entry.getKey());
+ appendEmptyTag(code);
+ appendTag("NUMBER", (String) entry.getValue());
+ }
+ });
+ }
+ }
+
+ private void appendAddress(final Map addr, final String code) {
+ if (addr.size() > 0) {
+ appendTag("ADR", true, new ContentBuilder() {
+ public void addTagContent() {
+ appendEmptyTag(code);
+
+ Iterator it = addr.entrySet().iterator();
+ while (it.hasNext()) {
+ final Map.Entry entry = (Map.Entry) it.next();
+ appendTag((String) entry.getKey(), (String) entry.getValue());
+ }
+ }
+ });
+ }
+ }
+
+ private void appendEmptyTag(Object tag) {
+ sb.append('<').append(tag).append("/>");
+ }
+
+ private void appendGenericFields() {
+ Iterator it = otherSimpleFields.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry entry = (Map.Entry) it.next();
+ appendTag(entry.getKey().toString(), (String) entry.getValue());
+ }
+ }
+
+ private void appendOrganization() {
+ if (hasOrganizationFields()) {
+ appendTag("ORG", true, new ContentBuilder() {
+ public void addTagContent() {
+ appendTag("ORGNAME", organization);
+ appendTag("ORGUNIT", organizationUnit);
+ }
+ });
+ }
+ }
+
+ private void appendField(String tag) {
+ String value = (String) otherSimpleFields.get(tag);
+ appendTag(tag, value);
+ }
+
+ private void appendFN() {
+ final ContentBuilder contentBuilder = new ContentBuilder() {
+ public void addTagContent() {
+ if (firstName != null) {
+ sb.append(firstName + ' ');
+ }
+ if (middleName != null) {
+ sb.append(middleName + ' ');
+ }
+ if (lastName != null) {
+ sb.append(lastName);
+ }
+ }
+ };
+ appendTag("FN", true, contentBuilder);
+ }
+
+ private void appendN() {
+ appendTag("N", true, new ContentBuilder() {
+ public void addTagContent() {
+ appendTag("FAMILY", lastName);
+ appendTag("GIVEN", firstName);
+ appendTag("MIDDLE", middleName);
+ }
+ });
+ }
+
+ private void appendTag(String tag, String attr, String attrValue, boolean hasContent,
+ ContentBuilder builder) {
+ sb.append('<').append(tag);
+ if (attr != null) {
+ sb.append(' ').append(attr).append('=').append('\'').append(attrValue).append('\'');
+ }
+
+ if (hasContent) {
+ sb.append('>');
+ builder.addTagContent();
+ sb.append("</").append(tag).append(">\n");
+ }
+ else {
+ sb.append("/>\n");
+ }
+ }
+
+ private void appendTag(String tag, boolean hasContent, ContentBuilder builder) {
+ appendTag(tag, null, null, hasContent, builder);
+ }
+
+ private void appendTag(String tag, final String tagText) {
+ if (tagText == null) return;
+ final ContentBuilder contentBuilder = new ContentBuilder() {
+ public void addTagContent() {
+ sb.append(tagText.trim());
+ }
+ };
+ appendTag(tag, true, contentBuilder);
+ }
+
+ }
+
+ //==============================================================
+
+ private interface ContentBuilder {
+
+ void addTagContent();
+ }
+
+ //==============================================================
+}
+
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Version.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Version.java
new file mode 100644
index 000000000..206208f3f
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Version.java
@@ -0,0 +1,132 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * A Version IQ packet, which is used by XMPP clients to discover version information
+ * about the software running at another entity's JID.<p>
+ *
+ * An example to discover the version of the server:
+ * <pre>
+ * // Request the version from the server.
+ * Version versionRequest = new Version();
+ * timeRequest.setType(IQ.Type.GET);
+ * timeRequest.setTo("example.com");
+ *
+ * // Create a packet collector to listen for a response.
+ * PacketCollector collector = con.createPacketCollector(
+ * new PacketIDFilter(versionRequest.getPacketID()));
+ *
+ * con.sendPacket(versionRequest);
+ *
+ * // Wait up to 5 seconds for a result.
+ * IQ result = (IQ)collector.nextResult(5000);
+ * if (result != null && result.getType() == IQ.Type.RESULT) {
+ * Version versionResult = (Version)result;
+ * // Do something with result...
+ * }</pre><p>
+ *
+ * @author Gaston Dombiak
+ */
+public class Version extends IQ {
+
+ private String name;
+ private String version;
+ private String os;
+
+ /**
+ * Returns the natural-language name of the software. This property will always be
+ * present in a result.
+ *
+ * @return the natural-language name of the software.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the natural-language name of the software. This message should only be
+ * invoked when parsing the XML and setting the property to a Version instance.
+ *
+ * @param name the natural-language name of the software.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the specific version of the software. This property will always be
+ * present in a result.
+ *
+ * @return the specific version of the software.
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * Sets the specific version of the software. This message should only be
+ * invoked when parsing the XML and setting the property to a Version instance.
+ *
+ * @param version the specific version of the software.
+ */
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ /**
+ * Returns the operating system of the queried entity. This property will always be
+ * present in a result.
+ *
+ * @return the operating system of the queried entity.
+ */
+ public String getOs() {
+ return os;
+ }
+
+ /**
+ * Sets the operating system of the queried entity. This message should only be
+ * invoked when parsing the XML and setting the property to a Version instance.
+ *
+ * @param os operating system of the queried entity.
+ */
+ public void setOs(String os) {
+ this.os = os;
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:version\">");
+ if (name != null) {
+ buf.append("<name>").append(name).append("</name>");
+ }
+ if (version != null) {
+ buf.append("<version>").append(version).append("</version>");
+ }
+ if (os != null) {
+ buf.append("<os>").append(os).append("</os>");
+ }
+ buf.append("</query>");
+ return buf.toString();
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/XHTMLExtension.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/XHTMLExtension.java
new file mode 100644
index 000000000..6bf266569
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/XHTMLExtension.java
@@ -0,0 +1,123 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.packet;
+
+import java.util.*;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * An XHTML sub-packet, which is used by XMPP clients to exchange formatted text. The XHTML
+ * extension is only a subset of XHTML 1.0.<p>
+ *
+ * The following link summarizes the requirements of XHTML IM:
+ * <a href="http://www.jabber.org/jeps/jep-0071.html#sect-id2598018">Valid tags</a>.<p>
+ *
+ * Warning: this is an non-standard protocol documented by
+ * <a href="http://www.jabber.org/jeps/jep-0071.html">JEP-71</a>. Because this is a
+ * non-standard protocol, it is subject to change.
+ *
+ * @author Gaston Dombiak
+ */
+public class XHTMLExtension implements PacketExtension {
+
+ private List bodies = new ArrayList();
+
+ /**
+ * Returns the XML element name of the extension sub-packet root element.
+ * Always returns "html"
+ *
+ * @return the XML element name of the packet extension.
+ */
+ public String getElementName() {
+ return "html";
+ }
+
+ /**
+ * Returns the XML namespace of the extension sub-packet root element.
+ * According the specification the namespace is always "http://jabber.org/protocol/xhtml-im"
+ *
+ * @return the XML namespace of the packet extension.
+ */
+ public String getNamespace() {
+ return "http://jabber.org/protocol/xhtml-im";
+ }
+
+ /**
+ * Returns the XML representation of a XHTML extension according the specification.
+ *
+ * Usually the XML representation will be inside of a Message XML representation like
+ * in the following example:
+ * <pre>
+ * &lt;message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"&gt;
+ * &lt;subject&gt;Any subject you want&lt;/subject&gt;
+ * &lt;body&gt;This message contains something interesting.&lt;/body&gt;
+ * &lt;html xmlns="http://jabber.org/protocol/xhtml-im"&gt;
+ * &lt;body&gt;&lt;p style='font-size:large'&gt;This message contains something &lt;em&gt;interesting&lt;/em&gt;.&lt;/p&gt;&lt;/body&gt;
+ * &lt;/html&gt;
+ * &lt;/message&gt;
+ * </pre>
+ *
+ */
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+ "\">");
+ // Loop through all the bodies and append them to the string buffer
+ for (Iterator i = getBodies(); i.hasNext();) {
+ buf.append((String) i.next());
+ }
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * Returns an Iterator for the bodies in the packet.
+ *
+ * @return an Iterator for the bodies in the packet.
+ */
+ public Iterator getBodies() {
+ synchronized (bodies) {
+ return Collections.unmodifiableList(new ArrayList(bodies)).iterator();
+ }
+ }
+
+ /**
+ * Adds a body to the packet.
+ *
+ * @param body the body to add.
+ */
+ public void addBody(String body) {
+ synchronized (bodies) {
+ bodies.add(body);
+ }
+ }
+
+ /**
+ * Returns a count of the bodies in the XHTML packet.
+ *
+ * @return the number of bodies in the XHTML packet.
+ */
+ public int getBodiesCount() {
+ return bodies.size();
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/package.html
new file mode 100644
index 000000000..490d1d72d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/package.html
@@ -0,0 +1 @@
+<body>XML packets that are part of the XMPP extension protocols.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/BytestreamsProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/BytestreamsProvider.java
new file mode 100644
index 000000000..0c42453fb
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/BytestreamsProvider.java
@@ -0,0 +1,104 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.packet.Bytestream;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses a bytestream packet.
+ *
+ * @author Alexander Wenckus
+ */
+public class BytestreamsProvider implements IQProvider {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser)
+ */
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser)
+ */
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser)
+ */
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ // StringBuffer buf = new StringBuffer();
+ boolean done = false;
+
+ Bytestream toReturn = new Bytestream();
+
+ String id = parser.getAttributeValue("", "sid");
+ String mode = parser.getAttributeValue("", "mode");
+
+ // streamhost
+ String JID = null;
+ String host = null;
+ String port = null;
+
+ int eventType;
+ String elementName;
+ // String namespace;
+ while (!done) {
+ eventType = parser.next();
+ elementName = parser.getName();
+ // namespace = parser.getNamespace();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (elementName.equals(Bytestream.StreamHost.ELEMENTNAME)) {
+ JID = parser.getAttributeValue("", "jid");
+ host = parser.getAttributeValue("", "host");
+ port = parser.getAttributeValue("", "port");
+ } else if (elementName
+ .equals(Bytestream.StreamHostUsed.ELEMENTNAME)) {
+ toReturn.setUsedHost(parser.getAttributeValue("", "jid"));
+ } else if (elementName.equals(Bytestream.Activate.ELEMENTNAME)) {
+ toReturn.setToActivate(parser.getAttributeValue("", "jid"));
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (elementName.equals("streamhost")) {
+ if (port == null) {
+ toReturn.addStreamHost(JID, host);
+ } else {
+ toReturn.addStreamHost(JID, host, Integer
+ .parseInt(port));
+ }
+ JID = null;
+ host = null;
+ port = null;
+ } else if (elementName.equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ toReturn.setMode((mode == "udp" ? Bytestream.Mode.UDP
+ : Bytestream.Mode.TCP));
+ toReturn.setSessionID(id);
+ return toReturn;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DataFormProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DataFormProvider.java
new file mode 100644
index 000000000..325a9ce2d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DataFormProvider.java
@@ -0,0 +1,160 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The DataFormProvider parses DataForm packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class DataFormProvider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new DataFormProvider.
+ * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+ */
+ public DataFormProvider() {
+ }
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ StringBuffer buffer = null;
+ DataForm dataForm = new DataForm(parser.getAttributeValue("", "type"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("instructions")) {
+ dataForm.addInstruction(parser.nextText());
+ }
+ else if (parser.getName().equals("title")) {
+ dataForm.setTitle(parser.nextText());
+ }
+ else if (parser.getName().equals("field")) {
+ dataForm.addField(parseField(parser));
+ }
+ else if (parser.getName().equals("item")) {
+ dataForm.addItem(parseItem(parser));
+ }
+ else if (parser.getName().equals("reported")) {
+ dataForm.setReportedData(parseReported(parser));
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(dataForm.getElementName())) {
+ done = true;
+ }
+ }
+ }
+ return dataForm;
+ }
+
+ private FormField parseField(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ FormField formField = new FormField(parser.getAttributeValue("", "var"));
+ formField.setLabel(parser.getAttributeValue("", "label"));
+ formField.setType(parser.getAttributeValue("", "type"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("desc")) {
+ formField.setDescription(parser.nextText());
+ }
+ else if (parser.getName().equals("value")) {
+ formField.addValue(parser.nextText());
+ }
+ else if (parser.getName().equals("required")) {
+ formField.setRequired(true);
+ }
+ else if (parser.getName().equals("option")) {
+ formField.addOption(parseOption(parser));
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("field")) {
+ done = true;
+ }
+ }
+ }
+ return formField;
+ }
+
+ private DataForm.Item parseItem(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ List fields = new ArrayList();
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("field")) {
+ fields.add(parseField(parser));
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ done = true;
+ }
+ }
+ }
+ return new DataForm.Item(fields);
+ }
+
+ private DataForm.ReportedData parseReported(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ List fields = new ArrayList();
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("field")) {
+ fields.add(parseField(parser));
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("reported")) {
+ done = true;
+ }
+ }
+ }
+ return new DataForm.ReportedData(fields);
+ }
+
+ private FormField.Option parseOption(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ FormField.Option option = null;
+ String label = parser.getAttributeValue("", "label");
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("value")) {
+ option = new FormField.Option(label, parser.nextText());
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("option")) {
+ done = true;
+ }
+ }
+ }
+ return option;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java
new file mode 100644
index 000000000..28bab2367
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java
@@ -0,0 +1,75 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.DelayInformation;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * The DelayInformationProvider parses DelayInformation packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class DelayInformationProvider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new DeliveryInformationProvider.
+ * ProviderManager requires that every PacketExtensionProvider has a public, no-argument
+ * constructor
+ */
+ public DelayInformationProvider() {
+ }
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ Date stamp = null;
+ try {
+ synchronized (DelayInformation.UTC_FORMAT) {
+ stamp = DelayInformation.UTC_FORMAT.parse(parser.getAttributeValue("", "stamp"));
+ }
+ } catch (ParseException e) {
+ // Try again but assuming that the date follows JEP-82 format
+ // (Jabber Date and Time Profiles)
+ try {
+ synchronized (DelayInformation.NEW_UTC_FORMAT) {
+ stamp = DelayInformation.NEW_UTC_FORMAT
+ .parse(parser.getAttributeValue("", "stamp"));
+ }
+ } catch (ParseException e1) {
+ // Last attempt. Try parsing the date assuming that it does not include milliseconds
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+ formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ stamp = formatter.parse(parser.getAttributeValue("", "stamp"));
+ }
+ }
+ DelayInformation delayInformation = new DelayInformation(stamp);
+ delayInformation.setFrom(parser.getAttributeValue("", "from"));
+ delayInformation.setReason(parser.nextText());
+ return delayInformation;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java
new file mode 100644
index 000000000..cf13d63f5
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java
@@ -0,0 +1,83 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+* The DiscoverInfoProvider parses Service Discovery information packets.
+*
+* @author Gaston Dombiak
+*/
+public class DiscoverInfoProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ DiscoverInfo discoverInfo = new DiscoverInfo();
+ boolean done = false;
+ DiscoverInfo.Feature feature = null;
+ DiscoverInfo.Identity identity = null;
+ String category = "";
+ String name = "";
+ String type = "";
+ String variable = "";
+ discoverInfo.setNode(parser.getAttributeValue("", "node"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("identity")) {
+ // Initialize the variables from the parsed XML
+ category = parser.getAttributeValue("", "category");
+ name = parser.getAttributeValue("", "name");
+ type = parser.getAttributeValue("", "type");
+ }
+ else if (parser.getName().equals("feature")) {
+ // Initialize the variables from the parsed XML
+ variable = parser.getAttributeValue("", "var");
+ }
+ // Otherwise, it must be a packet extension.
+ else {
+ discoverInfo.addExtension(PacketParserUtils.parsePacketExtension(parser
+ .getName(), parser.getNamespace(), parser));
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("identity")) {
+ // Create a new identity and add it to the discovered info.
+ identity = new DiscoverInfo.Identity(category, name);
+ identity.setType(type);
+ discoverInfo.addIdentity(identity);
+ }
+ if (parser.getName().equals("feature")) {
+ // Create a new feature and add it to the discovered info.
+ discoverInfo.addFeature(variable);
+ }
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ return discoverInfo;
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java
new file mode 100644
index 000000000..34aae227b
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java
@@ -0,0 +1,71 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.packet.*;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+* The DiscoverInfoProvider parses Service Discovery items packets.
+*
+* @author Gaston Dombiak
+*/
+public class DiscoverItemsProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ DiscoverItems discoverItems = new DiscoverItems();
+ boolean done = false;
+ DiscoverItems.Item item = null;
+ String jid = "";
+ String name = "";
+ String action = "";
+ String node = "";
+ discoverItems.setNode(parser.getAttributeValue("", "node"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ // Initialize the variables from the parsed XML
+ jid = parser.getAttributeValue("", "jid");
+ name = parser.getAttributeValue("", "name");
+ node = parser.getAttributeValue("", "node");
+ action = parser.getAttributeValue("", "action");
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ // Create a new Item and add it to DiscoverItems.
+ item = new DiscoverItems.Item(jid);
+ item.setName(name);
+ item.setNode(node);
+ item.setAction(action);
+ discoverItems.addItem(item);
+ }
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ return discoverItems;
+ }
+} \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/IBBProviders.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/IBBProviders.java
new file mode 100644
index 000000000..68af0e2c6
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/IBBProviders.java
@@ -0,0 +1,85 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.IBBExtensions;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * Parses an IBB packet.
+ *
+ * @author Alexander Wenckus
+ */
+public class IBBProviders {
+
+ /**
+ * Parses an open IBB packet.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public static class Open implements IQProvider {
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ final String sid = parser.getAttributeValue("", "sid");
+ final int blockSize = Integer.parseInt(parser.getAttributeValue("",
+ "block-size"));
+
+ return new IBBExtensions.Open(sid, blockSize);
+ }
+ }
+
+ /**
+ * Parses a data IBB packet.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public static class Data implements PacketExtensionProvider {
+ public PacketExtension parseExtension(XmlPullParser parser)
+ throws Exception {
+ final String sid = parser.getAttributeValue("", "sid");
+ final long seq = Long
+ .parseLong(parser.getAttributeValue("", "seq"));
+ final String data = parser.nextText();
+
+ return new IBBExtensions.Data(sid, seq, data);
+ }
+ }
+
+ /**
+ * Parses a close IBB packet.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public static class Close implements IQProvider {
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ final String sid = parser.getAttributeValue("", "sid");
+
+ return new IBBExtensions.Close(sid);
+ }
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java
new file mode 100644
index 000000000..0f5f04f07
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java
@@ -0,0 +1,81 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.packet.MUCAdmin;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The MUCAdminProvider parses MUCAdmin packets. (@see MUCAdmin)
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCAdminProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ MUCAdmin mucAdmin = new MUCAdmin();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ mucAdmin.addItem(parseItem(parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ return mucAdmin;
+ }
+
+ private MUCAdmin.Item parseItem(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ MUCAdmin.Item item =
+ new MUCAdmin.Item(
+ parser.getAttributeValue("", "affiliation"),
+ parser.getAttributeValue("", "role"));
+ item.setNick(parser.getAttributeValue("", "nick"));
+ item.setJid(parser.getAttributeValue("", "jid"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("actor")) {
+ item.setActor(parser.getAttributeValue("", "jid"));
+ }
+ if (parser.getName().equals("reason")) {
+ item.setReason(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ done = true;
+ }
+ }
+ }
+ return item;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java
new file mode 100644
index 000000000..aaa6afa88
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java
@@ -0,0 +1,108 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.provider.*;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.packet.MUCOwner;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The MUCOwnerProvider parses MUCOwner packets. (@see MUCOwner)
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCOwnerProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ MUCOwner mucOwner = new MUCOwner();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ mucOwner.addItem(parseItem(parser));
+ }
+ else if (parser.getName().equals("destroy")) {
+ mucOwner.setDestroy(parseDestroy(parser));
+ }
+ // Otherwise, it must be a packet extension.
+ else {
+ mucOwner.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(),
+ parser.getNamespace(), parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ return mucOwner;
+ }
+
+ private MUCOwner.Item parseItem(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ MUCOwner.Item item = new MUCOwner.Item(parser.getAttributeValue("", "affiliation"));
+ item.setNick(parser.getAttributeValue("", "nick"));
+ item.setRole(parser.getAttributeValue("", "role"));
+ item.setJid(parser.getAttributeValue("", "jid"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("actor")) {
+ item.setActor(parser.getAttributeValue("", "jid"));
+ }
+ if (parser.getName().equals("reason")) {
+ item.setReason(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ done = true;
+ }
+ }
+ }
+ return item;
+ }
+
+ private MUCOwner.Destroy parseDestroy(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ MUCOwner.Destroy destroy = new MUCOwner.Destroy();
+ destroy.setJid(parser.getAttributeValue("", "jid"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("reason")) {
+ destroy.setReason(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("destroy")) {
+ done = true;
+ }
+ }
+ }
+ return destroy;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCUserProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCUserProvider.java
new file mode 100644
index 000000000..25b6e8d6b
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCUserProvider.java
@@ -0,0 +1,174 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.provider.*;
+import org.jivesoftware.smackx.packet.*;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The MUCUserProvider parses packets with extended presence information about
+ * roles and affiliations.
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCUserProvider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new MUCUserProvider.
+ * ProviderManager requires that every PacketExtensionProvider has a public, no-argument
+ * constructor
+ */
+ public MUCUserProvider() {
+ }
+
+ /**
+ * Parses a MUCUser packet (extension sub-packet).
+ *
+ * @param parser the XML parser, positioned at the starting element of the extension.
+ * @return a PacketExtension.
+ * @throws Exception if a parsing error occurs.
+ */
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ MUCUser mucUser = new MUCUser();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("invite")) {
+ mucUser.setInvite(parseInvite(parser));
+ }
+ if (parser.getName().equals("item")) {
+ mucUser.setItem(parseItem(parser));
+ }
+ if (parser.getName().equals("password")) {
+ mucUser.setPassword(parser.nextText());
+ }
+ if (parser.getName().equals("status")) {
+ mucUser.setStatus(new MUCUser.Status(parser.getAttributeValue("", "code")));
+ }
+ if (parser.getName().equals("decline")) {
+ mucUser.setDecline(parseDecline(parser));
+ }
+ if (parser.getName().equals("destroy")) {
+ mucUser.setDestroy(parseDestroy(parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("x")) {
+ done = true;
+ }
+ }
+ }
+
+ return mucUser;
+ }
+
+ private MUCUser.Item parseItem(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ MUCUser.Item item =
+ new MUCUser.Item(
+ parser.getAttributeValue("", "affiliation"),
+ parser.getAttributeValue("", "role"));
+ item.setNick(parser.getAttributeValue("", "nick"));
+ item.setJid(parser.getAttributeValue("", "jid"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("actor")) {
+ item.setActor(parser.getAttributeValue("", "jid"));
+ }
+ if (parser.getName().equals("reason")) {
+ item.setReason(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ done = true;
+ }
+ }
+ }
+ return item;
+ }
+
+ private MUCUser.Invite parseInvite(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ MUCUser.Invite invite = new MUCUser.Invite();
+ invite.setFrom(parser.getAttributeValue("", "from"));
+ invite.setTo(parser.getAttributeValue("", "to"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("reason")) {
+ invite.setReason(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("invite")) {
+ done = true;
+ }
+ }
+ }
+ return invite;
+ }
+
+ private MUCUser.Decline parseDecline(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ MUCUser.Decline decline = new MUCUser.Decline();
+ decline.setFrom(parser.getAttributeValue("", "from"));
+ decline.setTo(parser.getAttributeValue("", "to"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("reason")) {
+ decline.setReason(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("decline")) {
+ done = true;
+ }
+ }
+ }
+ return decline;
+ }
+
+ private MUCUser.Destroy parseDestroy(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ MUCUser.Destroy destroy = new MUCUser.Destroy();
+ destroy.setJid(parser.getAttributeValue("", "jid"));
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("reason")) {
+ destroy.setReason(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("destroy")) {
+ done = true;
+ }
+ }
+ }
+ return destroy;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MessageEventProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MessageEventProvider.java
new file mode 100644
index 000000000..b93a0b9a4
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MessageEventProvider.java
@@ -0,0 +1,77 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.MessageEvent;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * The MessageEventProvider parses Message Event packets.
+*
+ * @author Gaston Dombiak
+ */
+public class MessageEventProvider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new MessageEventProvider.
+ * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+ */
+ public MessageEventProvider() {
+ }
+
+ /**
+ * Parses a MessageEvent packet (extension sub-packet).
+ *
+ * @param parser the XML parser, positioned at the starting element of the extension.
+ * @return a PacketExtension.
+ * @throws Exception if a parsing error occurs.
+ */
+ public PacketExtension parseExtension(XmlPullParser parser)
+ throws Exception {
+ MessageEvent messageEvent = new MessageEvent();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("id"))
+ messageEvent.setPacketID(parser.nextText());
+ if (parser.getName().equals(MessageEvent.COMPOSING))
+ messageEvent.setComposing(true);
+ if (parser.getName().equals(MessageEvent.DELIVERED))
+ messageEvent.setDelivered(true);
+ if (parser.getName().equals(MessageEvent.DISPLAYED))
+ messageEvent.setDisplayed(true);
+ if (parser.getName().equals(MessageEvent.OFFLINE))
+ messageEvent.setOffline(true);
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("x")) {
+ done = true;
+ }
+ }
+ }
+
+ return messageEvent;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java
new file mode 100644
index 000000000..4c3e35665
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java
@@ -0,0 +1,67 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.MultipleAddresses;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The MultipleAddressesProvider parses {@link MultipleAddresses} packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class MultipleAddressesProvider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new MultipleAddressesProvider.
+ * ProviderManager requires that every PacketExtensionProvider has a public, no-argument
+ * constructor.
+ */
+ public MultipleAddressesProvider() {
+ }
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ MultipleAddresses multipleAddresses = new MultipleAddresses();
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("address")) {
+ String type = parser.getAttributeValue("", "type");
+ String jid = parser.getAttributeValue("", "jid");
+ String node = parser.getAttributeValue("", "node");
+ String desc = parser.getAttributeValue("", "desc");
+ boolean delivered = "true".equals(parser.getAttributeValue("", "delivered"));
+ String uri = parser.getAttributeValue("", "uri");
+ // Add the parsed address
+ multipleAddresses.addAddress(type, jid, node, desc, delivered, uri);
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(multipleAddresses.getElementName())) {
+ done = true;
+ }
+ }
+ }
+ return multipleAddresses;
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java
new file mode 100644
index 000000000..7961da1bc
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java
@@ -0,0 +1,46 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.jivesoftware.smackx.packet.PrivateData;
+
+/**
+ * An interface for parsing custom private data. Each PrivateDataProvider must
+ * be registered with the PrivateDataManager class for it to be used. Every implementation
+ * of this interface <b>must</b> have a public, no-argument constructor.
+ *
+ * @author Matt Tucker
+ */
+public interface PrivateDataProvider {
+
+ /**
+ * Parse the private data sub-document and create a PrivateData instance. At the
+ * beginning of the method call, the xml parser will be positioned at the opening
+ * tag of the private data child element. At the end of the method call, the parser
+ * <b>must</b> be positioned on the closing tag of the child element.
+ *
+ * @param parser an XML parser.
+ * @return a new PrivateData instance.
+ * @throws Exception if an error occurs parsing the XML.
+ */
+ public PrivateData parsePrivateData(XmlPullParser parser) throws Exception;
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java
new file mode 100644
index 000000000..956e1328d
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java
@@ -0,0 +1,90 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import java.util.ArrayList;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.*;
+import org.jivesoftware.smackx.packet.*;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * The RosterExchangeProvider parses RosterExchange packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchangeProvider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new RosterExchangeProvider.
+ * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+ */
+ public RosterExchangeProvider() {
+ }
+
+ /**
+ * Parses a RosterExchange packet (extension sub-packet).
+ *
+ * @param parser the XML parser, positioned at the starting element of the extension.
+ * @return a PacketExtension.
+ * @throws Exception if a parsing error occurs.
+ */
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+
+ RosterExchange rosterExchange = new RosterExchange();
+ boolean done = false;
+ RemoteRosterEntry remoteRosterEntry = null;
+ String jid = "";
+ String name = "";
+ ArrayList groupsName = new ArrayList();
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ // Reset this variable since they are optional for each item
+ groupsName = new ArrayList();
+ // Initialize the variables from the parsed XML
+ jid = parser.getAttributeValue("", "jid");
+ name = parser.getAttributeValue("", "name");
+ }
+ if (parser.getName().equals("group")) {
+ groupsName.add(parser.nextText());
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ // Create packet.
+ remoteRosterEntry = new RemoteRosterEntry(jid, name, (String[]) groupsName.toArray(new String[groupsName.size()]));
+ rosterExchange.addRosterEntry(remoteRosterEntry);
+ }
+ if (parser.getName().equals("x")) {
+ done = true;
+ }
+ }
+ }
+
+ return rosterExchange;
+
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java
new file mode 100644
index 000000000..9a7106576
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java
@@ -0,0 +1,112 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.DelayInformation;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+import org.jivesoftware.smackx.packet.StreamInitiation.File;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The StreamInitiationProvider parses StreamInitiation packets.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class StreamInitiationProvider implements IQProvider {
+
+ public IQ parseIQ(final XmlPullParser parser) throws Exception {
+ boolean done = false;
+
+ // si
+ String id = parser.getAttributeValue("", "id");
+ String mimeType = parser.getAttributeValue("", "mime-type");
+
+ StreamInitiation initiation = new StreamInitiation();
+
+ // file
+ String name = null;
+ String size = null;
+ String hash = null;
+ String date = null;
+ String desc = null;
+ boolean isRanged = false;
+
+ // feature
+ DataForm form = null;
+ DataFormProvider dataFormProvider = new DataFormProvider();
+
+ int eventType;
+ String elementName;
+ String namespace;
+ while (!done) {
+ eventType = parser.next();
+ elementName = parser.getName();
+ namespace = parser.getNamespace();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (elementName.equals("file")) {
+ name = parser.getAttributeValue("", "name");
+ size = parser.getAttributeValue("", "size");
+ hash = parser.getAttributeValue("", "hash");
+ date = parser.getAttributeValue("", "date");
+ } else if (elementName.equals("desc")) {
+ desc = parser.nextText();
+ } else if (elementName.equals("range")) {
+ isRanged = true;
+ } else if (elementName.equals("x")
+ && namespace.equals("jabber:x:data")) {
+ form = (DataForm) dataFormProvider.parseExtension(parser);
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (elementName.equals("si")) {
+ done = true;
+ } else if (elementName.equals("file")) {
+ long fileSize = 0;
+ if(size != null && size.trim().length() !=0){
+ try {
+ fileSize = Long.parseLong(size);
+ }
+ catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+ File file = new File(name, fileSize);
+ file.setHash(hash);
+ if (date != null)
+ file.setDate(DelayInformation.UTC_FORMAT.parse(date));
+ file.setDesc(desc);
+ file.setRanged(isRanged);
+ initiation.setFile(file);
+ }
+ }
+ }
+
+ initiation.setSesssionID(id);
+ initiation.setMimeType(mimeType);
+
+ initiation.setFeatureNegotiationForm(form);
+
+ return initiation;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/VCardProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/VCardProvider.java
new file mode 100644
index 000000000..af45448b3
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/VCardProvider.java
@@ -0,0 +1,244 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.packet.VCard;
+import org.w3c.dom.*;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * vCard provider.
+ *
+ * @author Gaston Dombiak
+ */
+public class VCardProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ StringBuffer sb = new StringBuffer();
+ try {
+ int event = parser.getEventType();
+ // get the content
+ while (true) {
+ switch (event) {
+ case XmlPullParser.TEXT:
+ sb.append(parser.getText());
+ break;
+ case XmlPullParser.START_TAG:
+ sb.append('<').append(parser.getName()).append('>');
+ break;
+ case XmlPullParser.END_TAG:
+ sb.append("</").append(parser.getName()).append('>');
+ break;
+ default:
+ }
+
+ if (event == XmlPullParser.END_TAG && "vCard".equals(parser.getName())) break;
+
+ event = parser.next();
+ }
+ } catch (XmlPullParserException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ String xmlText = sb.toString();
+ return _createVCardFromXml(xmlText);
+ }
+
+ public static VCard _createVCardFromXml(String xmlText) {
+ VCard vCard = new VCard();
+ try {
+ DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+ Document document = documentBuilder.parse(new ByteArrayInputStream(xmlText.getBytes()));
+
+ new VCardReader(vCard, document).initializeFields();
+
+ } catch (Exception e) {
+ e.printStackTrace(System.err);
+ }
+ return vCard;
+ }
+
+ private static class VCardReader {
+ private final VCard vCard;
+ private final Document document;
+
+ VCardReader(VCard vCard, Document document) {
+ this.vCard = vCard;
+ this.document = document;
+ }
+
+ public void initializeFields() {
+ vCard.setFirstName(getTagContents("GIVEN"));
+ vCard.setLastName(getTagContents("FAMILY"));
+ vCard.setMiddleName(getTagContents("MIDDLE"));
+ vCard.setEncodedImage(getTagContents("BINVAL"));
+
+ setupEmails();
+
+ vCard.setOrganization(getTagContents("ORGNAME"));
+ vCard.setOrganizationUnit(getTagContents("ORGUNIT"));
+
+ setupSimpleFields();
+
+ setupPhones();
+ setupAddresses();
+ }
+
+ private void setupEmails() {
+ NodeList nodes = document.getElementsByTagName("USERID");
+ if (nodes == null) return;
+ for (int i = 0; i < nodes.getLength(); i++) {
+ Element element = (Element) nodes.item(i);
+ if ("HOME".equals(element.getParentNode().getFirstChild().getNodeName())) {
+ vCard.setEmailHome(getTextContent(element));
+ } else {
+ vCard.setEmailWork(getTextContent(element));
+ }
+ }
+ }
+
+ private void setupPhones() {
+ NodeList allPhones = document.getElementsByTagName("TEL");
+ if (allPhones == null) return;
+ for (int i = 0; i < allPhones.getLength(); i++) {
+ NodeList nodes = allPhones.item(i).getChildNodes();
+ String type = null;
+ String code = null;
+ String value = null;
+ for (int j = 0; j < nodes.getLength(); j++) {
+ Node node = nodes.item(j);
+ if (node.getNodeType() != Node.ELEMENT_NODE) continue;
+ String nodeName = node.getNodeName();
+ if ("NUMBER".equals(nodeName)) {
+ value = getTextContent(node);
+ }
+ else if (isWorkHome(nodeName)) {
+ type = nodeName;
+ }
+ else {
+ code = nodeName;
+ }
+ }
+ if (code == null || value == null) continue;
+ if ("HOME".equals(type)) {
+ vCard.setPhoneHome(code, value);
+ }
+ else { // By default, setup work phone
+ vCard.setPhoneWork(code, value);
+ }
+ }
+ }
+
+ private boolean isWorkHome(String nodeName) {
+ return "HOME".equals(nodeName) || "WORK".equals(nodeName);
+ }
+
+ private void setupAddresses() {
+ NodeList allAddresses = document.getElementsByTagName("ADR");
+ if (allAddresses == null) return;
+ for (int i = 0; i < allAddresses.getLength(); i++) {
+ Element addressNode = (Element) allAddresses.item(i);
+
+ String type = null;
+ List code = new ArrayList();
+ List value = new ArrayList();
+ NodeList childNodes = addressNode.getChildNodes();
+ for(int j = 0; j < childNodes.getLength(); j++) {
+ Node node = childNodes.item(j);
+ if (node.getNodeType() != Node.ELEMENT_NODE) continue;
+ String nodeName = node.getNodeName();
+ if (isWorkHome(nodeName)) {
+ type = nodeName;
+ }
+ else {
+ code.add(nodeName);
+ value.add(getTextContent(node));
+ }
+ }
+ for (int j = 0; j < value.size(); j++) {
+ if ("HOME".equals(type)) {
+ vCard.setAddressFieldHome((String) code.get(j), (String) value.get(j));
+ }
+ else { // By default, setup work address
+ vCard.setAddressFieldWork((String) code.get(j), (String) value.get(j));
+ }
+ }
+ }
+ }
+
+ private String getTagContents(String tag) {
+ NodeList nodes = document.getElementsByTagName(tag);
+ if (nodes != null && nodes.getLength() == 1) {
+ return getTextContent(nodes.item(0));
+ }
+ return null;
+ }
+
+ private void setupSimpleFields() {
+ NodeList childNodes = document.getDocumentElement().getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node node = childNodes.item(i);
+ if (node instanceof Element) {
+ Element element = (Element) node;
+
+ String field = element.getNodeName();
+ if (element.getChildNodes().getLength() == 0) {
+ vCard.setField(field, "");
+ } else if (element.getChildNodes().getLength() == 1 &&
+ element.getChildNodes().item(0) instanceof Text) {
+ vCard.setField(field, getTextContent(element));
+ }
+ }
+ }
+ }
+
+ private String getTextContent(Node node) {
+ StringBuffer result = new StringBuffer();
+ appendText(result, node);
+ return result.toString();
+ }
+
+ private void appendText(StringBuffer result, Node node) {
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node nd = childNodes.item(i);
+ String nodeValue = nd.getNodeValue();
+ if (nodeValue != null) {
+ result.append(nodeValue);
+ }
+ appendText(result, nd);
+ }
+ }
+ }
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java
new file mode 100644
index 000000000..7970c7907
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java
@@ -0,0 +1,78 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2004 Jive Software.
+ *
+ * All rights reserved. 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.XHTMLExtension;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The XHTMLExtensionProvider parses XHTML packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class XHTMLExtensionProvider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new XHTMLExtensionProvider.
+ * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+ */
+ public XHTMLExtensionProvider() {
+ }
+
+ /**
+ * Parses a XHTMLExtension packet (extension sub-packet).
+ *
+ * @param parser the XML parser, positioned at the starting element of the extension.
+ * @return a PacketExtension.
+ * @throws Exception if a parsing error occurs.
+ */
+ public PacketExtension parseExtension(XmlPullParser parser)
+ throws Exception {
+ XHTMLExtension xhtmlExtension = new XHTMLExtension();
+ boolean done = false;
+ StringBuffer buffer = new StringBuffer();;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("body"))
+ buffer = new StringBuffer();
+ buffer.append(parser.getText());
+ } else if (eventType == XmlPullParser.TEXT) {
+ if (buffer != null) buffer.append(parser.getText());
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("body")) {
+ buffer.append(parser.getText());
+ xhtmlExtension.addBody(buffer.toString());
+ }
+ else if (parser.getName().equals(xhtmlExtension.getElementName())) {
+ done = true;
+ }
+ else
+ buffer.append(parser.getText());
+ }
+ }
+
+ return xhtmlExtension;
+ }
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/package.html
new file mode 100644
index 000000000..962ba6372
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/package.html
@@ -0,0 +1 @@
+<body>Provides pluggable parsing logic for Smack extensions.</body> \ No newline at end of file
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/SimpleUserSearch.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/SimpleUserSearch.java
new file mode 100644
index 000000000..4ab810e89
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/SimpleUserSearch.java
@@ -0,0 +1,145 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2005 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+package org.jivesoftware.smackx.search;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.ReportedData;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * SimpleUserSearch is used to support the non-dataform type of JEP 55. This provides
+ * the mechanism for allowing always type ReportedData to be returned by any search result,
+ * regardless of the form of the data returned from the server.
+ *
+ * @author Derek DeMoro
+ */
+class SimpleUserSearch extends IQ {
+
+ private Form form;
+ private ReportedData data;
+
+ public void setForm(Form form) {
+ this.form = form;
+ }
+
+ public ReportedData getReportedData() {
+ return data;
+ }
+
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:search\">");
+ buf.append(getItemsToSearch());
+ buf.append("</query>");
+ return buf.toString();
+ }
+
+ private String getItemsToSearch() {
+ StringBuffer buf = new StringBuffer();
+
+ if (form == null) {
+ form = Form.getFormFrom(this);
+ }
+
+ if (form == null) {
+ return "";
+ }
+
+ Iterator fields = form.getFields();
+ while (fields.hasNext()) {
+ FormField field = (FormField) fields.next();
+ String name = field.getVariable();
+ String value = getSingleValue(field);
+ if (value.trim().length() > 0) {
+ buf.append("<").append(name).append(">").append(value).append("</").append(name).append(">");
+ }
+ }
+
+ return buf.toString();
+ }
+
+ private static String getSingleValue(FormField formField) {
+ Iterator values = formField.getValues();
+ while (values.hasNext()) {
+ return (String) values.next();
+ }
+ return "";
+ }
+
+ protected void parseItems(XmlPullParser parser) throws Exception {
+ ReportedData data = new ReportedData();
+ data.addColumn(new ReportedData.Column("JID", "jid", "text-single"));
+
+ boolean done = false;
+
+ List fields = new ArrayList();
+ while (!done) {
+ if (parser.getAttributeCount() > 0) {
+ String jid = parser.getAttributeValue("", "jid");
+ List valueList = new ArrayList();
+ valueList.add(jid);
+ ReportedData.Field field = new ReportedData.Field("jid", valueList);
+ fields.add(field);
+ }
+
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG && parser.getName().equals("item")) {
+ fields = new ArrayList();
+ }
+ else if (eventType == XmlPullParser.END_TAG && parser.getName().equals("item")) {
+ ReportedData.Row row = new ReportedData.Row(fields);
+ data.addRow(row);
+ }
+ else if (eventType == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ String value = parser.nextText();
+
+ List valueList = new ArrayList();
+ valueList.add(value);
+ ReportedData.Field field = new ReportedData.Field(name, valueList);
+ fields.add(field);
+
+ boolean exists = false;
+ Iterator cols = data.getColumns();
+ while (cols.hasNext()) {
+ ReportedData.Column column = (ReportedData.Column) cols.next();
+ if (column.getVariable().equals(name)) {
+ exists = true;
+ }
+ }
+
+ // Column name should be the same
+ if (!exists) {
+ ReportedData.Column column = new ReportedData.Column(name, name, "text-single");
+ data.addColumn(column);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+
+ }
+
+ this.data = data;
+ }
+
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearch.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearch.java
new file mode 100644
index 000000000..400344530
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearch.java
@@ -0,0 +1,249 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2005 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+package org.jivesoftware.smackx.search;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.ReportedData;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Implements the protocol currently used to search information repositories on the Jabber network. To date, the jabber:iq:search protocol
+ * has been used mainly to search for people who have registered with user directories (e.g., the "Jabber User Directory" hosted at users.jabber.org).
+ * However, the jabber:iq:search protocol is not limited to user directories, and could be used to search other Jabber information repositories
+ * (such as chatroom directories) or even to provide a Jabber interface to conventional search engines.
+ * <p/>
+ * The basic functionality is to query an information repository regarding the possible search fields, to send a search query, and to receive search results.
+ *
+ * @author Derek DeMoro
+ */
+public class UserSearch extends IQ {
+
+ /**
+ * Creates a new instance of UserSearch.
+ */
+ public UserSearch() {
+ }
+
+ public String getChildElementXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<query xmlns=\"jabber:iq:search\">");
+ buf.append(getExtensionsXML());
+ buf.append("</query>");
+ return buf.toString();
+ }
+
+ /**
+ * Returns the form for all search fields supported by the search service.
+ *
+ * @param con the current XMPPConnection.
+ * @param searchService the search service to use. (ex. search.jivesoftware.com)
+ * @return the search form received by the server.
+ * @throws org.jivesoftware.smack.XMPPException
+ * thrown if a server error has occurred.
+ */
+ public Form getSearchForm(XMPPConnection con, String searchService) throws XMPPException {
+ UserSearch search = new UserSearch();
+ search.setType(IQ.Type.GET);
+ search.setTo(searchService);
+
+ PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID()));
+ con.sendPacket(search);
+
+ IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return Form.getFormFrom(response);
+ }
+
+ /**
+ * Sends the filled out answer form to be sent and queried by the search service.
+ *
+ * @param con the current XMPPConnection.
+ * @param searchForm the <code>Form</code> to send for querying.
+ * @param searchService the search service to use. (ex. search.jivesoftware.com)
+ * @return ReportedData the data found from the query.
+ * @throws org.jivesoftware.smack.XMPPException
+ * thrown if a server error has occurred.
+ */
+ public ReportedData sendSearchForm(XMPPConnection con, Form searchForm, String searchService) throws XMPPException {
+ UserSearch search = new UserSearch();
+ search.setType(IQ.Type.SET);
+ search.setTo(searchService);
+ search.addExtension(searchForm.getDataFormToSend());
+
+ PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID()));
+
+ con.sendPacket(search);
+
+ IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ return sendSimpleSearchForm(con, searchForm, searchService);
+ }
+
+
+ return ReportedData.getReportedDataFrom(response);
+ }
+
+ /**
+ * Sends the filled out answer form to be sent and queried by the search service.
+ *
+ * @param con the current XMPPConnection.
+ * @param searchForm the <code>Form</code> to send for querying.
+ * @param searchService the search service to use. (ex. search.jivesoftware.com)
+ * @return ReportedData the data found from the query.
+ * @throws org.jivesoftware.smack.XMPPException
+ * thrown if a server error has occurred.
+ */
+ public ReportedData sendSimpleSearchForm(XMPPConnection con, Form searchForm, String searchService) throws XMPPException {
+ SimpleUserSearch search = new SimpleUserSearch();
+ search.setForm(searchForm);
+ search.setType(IQ.Type.SET);
+ search.setTo(searchService);
+
+ PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID()));
+
+ con.sendPacket(search);
+
+ IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+
+ if (response instanceof SimpleUserSearch) {
+ return ((SimpleUserSearch) response).getReportedData();
+ }
+ return null;
+ }
+
+ /**
+ * Internal Search service Provider.
+ */
+ public static class Provider implements IQProvider {
+
+ /**
+ * Provider Constructor.
+ */
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ UserSearch search = null;
+ SimpleUserSearch simpleUserSearch = new SimpleUserSearch();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG && parser.getName().equals("instructions")) {
+ buildDataForm(simpleUserSearch, parser.nextText(), parser);
+ return simpleUserSearch;
+ }
+ else if (eventType == XmlPullParser.START_TAG && parser.getName().equals("item")) {
+ simpleUserSearch.parseItems(parser);
+ return simpleUserSearch;
+ }
+ else if (eventType == XmlPullParser.START_TAG && parser.getNamespace().equals("jabber:x:data")) {
+ // Otherwise, it must be a packet extension.
+ search = new UserSearch();
+ search.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(),
+ parser.getNamespace(), parser));
+
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ if (search != null) {
+ return search;
+ }
+ return simpleUserSearch;
+ }
+ }
+
+ private static void buildDataForm(SimpleUserSearch search, String instructions, XmlPullParser parser) throws Exception {
+ DataForm dataForm = new DataForm(Form.TYPE_FORM);
+ boolean done = false;
+ dataForm.setTitle("User Search");
+ dataForm.addInstruction(instructions);
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG && !parser.getNamespace().equals("jabber:x:data")) {
+ String name = parser.getName();
+ FormField field = new FormField(name);
+
+ // Handle hard coded values.
+ if(name.equals("first")){
+ field.setLabel("First Name");
+ }
+ else if(name.equals("last")){
+ field.setLabel("Last Name");
+ }
+ else if(name.equals("email")){
+ field.setLabel("Email Address");
+ }
+ else if(name.equals("nick")){
+ field.setLabel("Nickname");
+ }
+
+ field.setType(FormField.TYPE_TEXT_SINGLE);
+ dataForm.addField(field);
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ else if (eventType == XmlPullParser.START_TAG && parser.getNamespace().equals("jabber:x:data")) {
+ search.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(),
+ parser.getNamespace(), parser));
+ done = true;
+ }
+ }
+ if (search.getExtension("x", "jabber:x:data") == null) {
+ search.addExtension(dataForm);
+ }
+ }
+
+
+}
diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearchManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearchManager.java
new file mode 100644
index 000000000..c771d6925
--- /dev/null
+++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearchManager.java
@@ -0,0 +1,109 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright (C) 1999-2005 Jive Software. All rights reserved.
+ *
+ * This software is the proprietary information of Jive Software.
+ * Use is subject to license terms.
+ */
+package org.jivesoftware.smackx.search;
+
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.ReportedData;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The UserSearchManager is a facade built upon Jabber Search Services (JEP-055) to allow for searching
+ * repositories on a Jabber Server. This implementation allows for transparency of implementation of
+ * searching (DataForms or No DataForms), but allows the user to simply use the DataForm model for both
+ * types of support.
+ * <pre>
+ * XMPPConnection con = new XMPPConnection("jabber.org");
+ * con.login("john", "doe");
+ * UserSearchManager search = new UserSearchManager(con, "users.jabber.org");
+ * Form searchForm = search.getSearchForm();
+ * Form answerForm = searchForm.createAnswerForm();
+ * answerForm.setAnswer("last", "DeMoro");
+ * ReportedData data = search.getSearchResults(answerForm);
+ * // Use Returned Data
+ * </pre>
+ *
+ * @author Derek DeMoro
+ */
+public class UserSearchManager {
+
+ private XMPPConnection con;
+ private UserSearch userSearch;
+
+ /**
+ * Creates a new UserSearchManager.
+ *
+ * @param con the XMPPConnection to use.
+ */
+ public UserSearchManager(XMPPConnection con) {
+ this.con = con;
+ userSearch = new UserSearch();
+ }
+
+ /**
+ * Returns the form to fill out to perform a search.
+ *
+ * @param searchService the search service to query.
+ * @return the form to fill out to perform a search.
+ * @throws XMPPException thrown if a server error has occurred.
+ */
+ public Form getSearchForm(String searchService) throws XMPPException {
+ return userSearch.getSearchForm(con, searchService);
+ }
+
+ /**
+ * Submits a search form to the server and returns the resulting information
+ * in the form of <code>ReportedData</code>
+ *
+ * @param searchForm the <code>Form</code> to submit for searching.
+ * @param searchService the name of the search service to use.
+ * @return the ReportedData returned by the server.
+ * @throws XMPPException thrown if a server error has occurred.
+ */
+ public ReportedData getSearchResults(Form searchForm, String searchService) throws XMPPException {
+ return userSearch.sendSearchForm(con, searchForm, searchService);
+ }
+
+
+ /**
+ * Returns a collection of search services found on the server.
+ *
+ * @return a Collection of search services found on the server.
+ * @throws XMPPException thrown if a server error has occurred.
+ */
+ public Collection getSearchServices() throws XMPPException {
+ List searchServices = new ArrayList();
+ ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(con);
+ DiscoverItems items = discoManager.discoverItems(con.getServiceName());
+ for (Iterator it = items.getItems(); it.hasNext();) {
+ DiscoverItems.Item item = (DiscoverItems.Item) it.next();
+ try {
+ DiscoverInfo info = discoManager.discoverInfo(item.getEntityID());
+ if (info.containsFeature("jabber:iq:search")) {
+ searchServices.add(item.getEntityID());
+ }
+ }
+ catch (XMPPException e) {
+ // No info found.
+ break;
+ }
+ }
+ return searchServices;
+ }
+}

Back to the top