diff --git a/documentation/extensions/muc.md b/documentation/extensions/muc.md index 0eb02d388..b20d7cb16 100644 --- a/documentation/extensions/muc.md +++ b/documentation/extensions/muc.md @@ -38,8 +38,8 @@ the _**MultiUserChat**_ instance where nickname is the nickname to use when joining the room. Depending on the type of room that you want to create you will have to use -different configuration forms. In order to create an Instant room just send -**sendConfigurationForm(Form form)** where form is an empty form. But if you +different configuration forms. In order to create an Instant room just use +`MucCreateConfigFormHandle.makeInstant()`. But if you want to create a Reserved room then you should first get the room's configuration form, complete the form and finally send it back to the server. @@ -54,12 +54,8 @@ MultiUserChatManager manager = MultiUserChatManager.getInstanceFor(connection); // Get a MultiUserChat using MultiUserChatManager MultiUserChat muc = manager.getMultiUserChat("myroom@conference.jabber.org"); -// Create the room -muc.create("testbot"); - -// Send an empty room configuration form which indicates that we want -// an instant room -muc.sendConfigurationForm(new Form(DataForm.Type.submit)); +// Create the room and send an empty configuration form to make this an instant room +muc.create("testbot").makeInstant(); ``` In this example we can see how to create a reserved room. The form is @@ -72,27 +68,14 @@ MultiUserChatManager manager = MultiUserChatManager.getInstanceFor(connection); // Create a MultiUserChat using an XMPPConnection for a room MultiUserChat muc = = manager.getMultiUserChat("myroom@conference.jabber.org"); -// Create the room -muc.create("testbot"); +// Prepare a list of owners of the new room +Set owners = JidUtil.jidSetFrom(new String[] { "me@example.org", "juliet@example.org" }); -// Get the the room's configuration form -Form form = muc.getConfigurationForm(); -// Create a new form to submit based on the original form -Form submitForm = form.createAnswerForm(); -// Add default answers to the form to submit -for (Iterator fields = form.getFields(); fields.hasNext();) { - FormField field = (FormField) fields.next(); - if (!FormField.type.hidden.equals(field.getType()) && field.getVariable() != null) { - // Sets the default value as the answer - submitForm.setDefaultAnswer(field.getVariable()); -} -} -// Sets the new owner of the room -List owners = new ArrayList(); -owners.add("johndoe@jabber.org"); -submitForm.setAnswer("muc#roomconfig_roomowners", owners); -// Send the completed form (with default values) to the server to configure the room -muc.sendConfigurationForm(submitForm); +// Create the room +muc.create("testbot") + .getConfigFormManger() + .setRoomOwners(owners) + .submitConfigurationForm(); ``` Join a room diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkManager.java index 8b6fc5808..33e9235e1 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkManager.java @@ -28,6 +28,8 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smackx.iqprivate.PrivateDataManager; +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.parts.Resourcepart; /** @@ -109,8 +111,8 @@ public final class BookmarkManager { * @throws NotConnectedException * @throws InterruptedException */ - public void addBookmarkedConference(String name, String jid, boolean isAutoJoin, - String nickname, String password) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException + public void addBookmarkedConference(String name, BareJid jid, boolean isAutoJoin, + Resourcepart nickname, String password) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { retrieveBookmarks(); BookmarkedConference bookmark @@ -144,12 +146,12 @@ public final class BookmarkManager { * @throws IllegalArgumentException thrown when the conference being removed is a shared * conference */ - public void removeBookmarkedConference(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + public void removeBookmarkedConference(BareJid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { retrieveBookmarks(); Iterator it = bookmarks.getBookmarkedConferences().iterator(); while(it.hasNext()) { BookmarkedConference conference = it.next(); - if(conference.getJid().equalsIgnoreCase(jid)) { + if(conference.getJid().equals(jid)) { if(conference.isShared()) { throw new IllegalArgumentException("Conference is shared and can't be removed"); } @@ -230,6 +232,22 @@ public final class BookmarkManager { } } + /** + * Check if the service supports bookmarks using private data. + * + * @return true if the service supports private data, false otherwise. + * @throws NoResponseException + * @throws NotConnectedException + * @throws InterruptedException + * @throws XMPPErrorException + * @see PrivateDataManager#isSupported() + * @since 4.2 + */ + public boolean isSupported() throws NoResponseException, NotConnectedException, + XMPPErrorException, InterruptedException { + return privateDataManager.isSupported(); + } + private Bookmarks retrieveBookmarks() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { synchronized(bookmarkLock) { if(bookmarks == null) { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkedConference.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkedConference.java index 74e7fd85c..c6cfd65fd 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkedConference.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/BookmarkedConference.java @@ -17,6 +17,9 @@ package org.jivesoftware.smackx.bookmarks; +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.parts.Resourcepart; + /** * Respresents a Conference Room bookmarked on the server using XEP-0048 Bookmark Storage XEP. * @@ -26,17 +29,17 @@ public class BookmarkedConference implements SharedBookmark { private String name; private boolean autoJoin; - private final String jid; + private final BareJid jid; - private String nickname; + private Resourcepart nickname; private String password; private boolean isShared; - protected BookmarkedConference(String jid) { + protected BookmarkedConference(BareJid jid) { this.jid = jid; } - protected BookmarkedConference(String name, String jid, boolean autoJoin, String nickname, + protected BookmarkedConference(String name, BareJid jid, boolean autoJoin, Resourcepart nickname, String password) { this.name = name; @@ -78,7 +81,7 @@ public class BookmarkedConference implements SharedBookmark { * * @return the full JID of this conference room. */ - public String getJid() { + public BareJid getJid() { return jid; } @@ -88,11 +91,11 @@ public class BookmarkedConference implements SharedBookmark { * * @return the nickname to use when joining, null may be returned. */ - public String getNickname() { + public Resourcepart getNickname() { return nickname; } - protected void setNickname(String nickname) { + protected void setNickname(Resourcepart nickname) { this.nickname = nickname; } @@ -116,7 +119,7 @@ public class BookmarkedConference implements SharedBookmark { return false; } BookmarkedConference conference = (BookmarkedConference)obj; - return conference.getJid().equalsIgnoreCase(jid); + return conference.getJid().equals(jid); } @Override diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/Bookmarks.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/Bookmarks.java index 31fc6b879..b9f3ac799 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/Bookmarks.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bookmarks/Bookmarks.java @@ -16,9 +16,12 @@ */ package org.jivesoftware.smackx.bookmarks; +import org.jivesoftware.smack.util.ParserUtils; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smackx.iqprivate.packet.PrivateData; import org.jivesoftware.smackx.iqprivate.provider.PrivateDataProvider; +import org.jxmpp.jid.BareJid; +import org.jxmpp.jid.parts.Resourcepart; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -268,7 +271,7 @@ public class Bookmarks implements PrivateData { private static BookmarkedConference getConferenceStorage(XmlPullParser parser) throws XmlPullParserException, IOException { String name = parser.getAttributeValue("", "name"); String autojoin = parser.getAttributeValue("", "autojoin"); - String jid = parser.getAttributeValue("", "jid"); + BareJid jid = ParserUtils.getBareJidAttribute(parser); BookmarkedConference conf = new BookmarkedConference(jid); conf.setName(name); @@ -279,7 +282,8 @@ public class Bookmarks implements PrivateData { while (!done) { int eventType = parser.next(); if (eventType == XmlPullParser.START_TAG && "nick".equals(parser.getName())) { - conf.setNickname(parser.nextText()); + String nickString = parser.nextText(); + conf.setNickname(Resourcepart.from(nickString)); } else if (eventType == XmlPullParser.START_TAG && "password".equals(parser.getName())) { conf.setPassword(parser.nextText()); diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/PrivateDataManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/PrivateDataManager.java index 6537b18be..6b06c6869 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/PrivateDataManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/iqprivate/PrivateDataManager.java @@ -24,6 +24,7 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.XMPPError.Condition; import org.jivesoftware.smack.provider.IQProvider; import org.jivesoftware.smackx.iqprivate.packet.DefaultPrivateData; import org.jivesoftware.smackx.iqprivate.packet.PrivateData; @@ -184,6 +185,51 @@ public final class PrivateDataManager extends Manager { connection().createPacketCollectorAndSend(privateDataSet).nextResultOrThrow(); } + private static final PrivateData DUMMY_PRIVATE_DATA = new PrivateData() { + @Override + public String getElementName() { + return "smackDummyPrivateData"; + } + + @Override + public String getNamespace() { + return "https://igniterealtime.org/projects/smack/"; + } + + @Override + public CharSequence toXML() { + return '<' + getElementName() + " xmlns='" + getNamespace() + "'/>"; + } + }; + + /** + * Check if the service supports private data. + * + * @return true if the service supports private data, false otherwise. + * @throws NoResponseException + * @throws NotConnectedException + * @throws InterruptedException + * @throws XMPPErrorException + * @since 4.2 + */ + public boolean isSupported() throws NoResponseException, NotConnectedException, + InterruptedException, XMPPErrorException { + // This is just a primitive hack, since XEP-49 does not specify a way to determine if the + // service supports it + try { + setPrivateData(DUMMY_PRIVATE_DATA); + return true; + } + catch (XMPPErrorException e) { + if (e.getXMPPError().getCondition() == Condition.service_unavailable) { + return false; + } + else { + throw e; + } + } + } + /** * An IQ provider to parse IQ results containing private data. */ diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java new file mode 100644 index 000000000..ef05af2cd --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MucConfigFormManager.java @@ -0,0 +1,116 @@ +/** + * + * Copyright 2015 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.muc; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucConfigurationNotSupported; +import org.jivesoftware.smackx.xdata.Form; +import org.jivesoftware.smackx.xdata.FormField; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.util.JidUtil; + +/** + * Multi-User Chat configuration form manager is used to fill out and submit a {@link Form} used to + * configure rooms. + *

+ * Room configuration needs either be done right after the room is created and still locked. Or at + * any later point (see XEP-45 § 10.2 + * Subsequent Room Configuration). When done with the configuration, call + * {@link #submitConfigurationForm()}. + *

+ *

+ * The manager may not provide all possible configuration options. If you want direct access to the + * configuraiton form, use {@link MultiUserChat#getConfigurationForm()} and + * {@link MultiUserChat#sendConfigurationForm(Form)}. + *

+ */ +public class MucConfigFormManager { + public static final String MUC_ROOMCONFIG_ROOMOWNERS = "muc#roomconfig_roomowners"; + + private final MultiUserChat multiUserChat; + private final Form answerForm; + private final List owners; + + /** + * Create a new MUC config form manager. + *

+ * Note that the answerForm needs to be filled out with the defaults. + *

+ * + * @param multiUserChat the MUC for this configuration form. + * @throws InterruptedException + * @throws NotConnectedException + * @throws XMPPErrorException + * @throws NoResponseException + */ + MucConfigFormManager(MultiUserChat multiUserChat) throws NoResponseException, + XMPPErrorException, NotConnectedException, InterruptedException { + this.multiUserChat = multiUserChat; + + // Set the answer form + Form configForm = multiUserChat.getConfigurationForm(); + this.answerForm = configForm.createAnswerForm(); + // Add the default answers to the form to submit + for (FormField field : configForm.getFields()) { + if (field.getType() == FormField.Type.hidden + || StringUtils.isNullOrEmpty(field.getVariable())) { + continue; + } + answerForm.setDefaultAnswer(field.getVariable()); + } + + // Set the local variables according to the fields found in the answer form + if (answerForm.hasField(MUC_ROOMCONFIG_ROOMOWNERS)) { + // Set 'owners' to the currently configured owners + List ownerStrings = answerForm.getField(MUC_ROOMCONFIG_ROOMOWNERS).getValues(); + owners = new ArrayList<>(ownerStrings.size()); + JidUtil.jidsFrom(ownerStrings, owners, null); + } + else { + // roomowners not supported, this should barely be the case + owners = null; + } + } + + public boolean supportsRoomOwners() { + return owners != null; + } + + public MucConfigFormManager setRoomOwners(Collection newOwners) throws MucConfigurationNotSupported { + if (!supportsRoomOwners()) { + throw new MucConfigurationNotSupported(MUC_ROOMCONFIG_ROOMOWNERS); + } + owners.clear(); + owners.addAll(newOwners); + return this; + } + + public void submitConfigurationForm() throws NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException { + if (owners != null) { + answerForm.setAnswer(MUC_ROOMCONFIG_ROOMOWNERS, JidUtil.toStringList(owners)); + } + multiUserChat.sendConfigurationForm(answerForm); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index 85a584665..7d341c5f9 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -57,6 +57,9 @@ import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.iqregister.packet.Registration; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MucNotJoinedException; +import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException; import org.jivesoftware.smackx.muc.packet.Destroy; import org.jivesoftware.smackx.muc.packet.MUCAdmin; import org.jivesoftware.smackx.muc.packet.MUCInitialPresence; @@ -354,25 +357,30 @@ public class MultiUserChat { * the room. {@link #sendConfigurationForm(Form)} * * @param nickname the nickname to use. + * @return a handle to the MUC create configuration form API. * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if * the user is not allowed to create the room) * @throws NoResponseException if there was no response from the server. - * @throws SmackException If the creation failed because of a missing acknowledge from the - * server, e.g. because the room already existed. * @throws InterruptedException + * @throws NotConnectedException + * @throws MucAlreadyJoinedException + * @throws MissingMucCreationAcknowledgeException */ - public synchronized void create(Resourcepart nickname) throws NoResponseException, XMPPErrorException, SmackException, InterruptedException { + public synchronized MucCreateConfigFormHandle create(Resourcepart nickname) throws NoResponseException, + XMPPErrorException, InterruptedException, MucAlreadyJoinedException, + NotConnectedException, MissingMucCreationAcknowledgeException { if (joined) { - throw new IllegalStateException("Creation failed - User already joined the room."); + throw new MucAlreadyJoinedException(); } - if (createOrJoin(nickname)) { + MucCreateConfigFormHandle mucCreateConfigFormHandle = createOrJoin(nickname); + if (mucCreateConfigFormHandle != null) { // We successfully created a new room - return; + return mucCreateConfigFormHandle; } // We need to leave the room since it seems that the room already existed leave(); - throw new SmackException("Creation failed - Missing acknowledge of room creation."); + throw new MissingMucCreationAcknowledgeException(); } /** @@ -380,15 +388,16 @@ public class MultiUserChat { * discussion history and using the connections default reply timeout. * * @param nickname - * @return true if the room creation was acknowledged by the service, false otherwise. + * @return A {@link MucCreateConfigFormHandle} if the room was created, or {code null} if the room was joined. * @throws NoResponseException * @throws XMPPErrorException - * @throws SmackException * @throws InterruptedException + * @throws NotConnectedException + * @throws MucAlreadyJoinedException * @see #createOrJoin(Resourcepart, String, DiscussionHistory, long) */ - public synchronized boolean createOrJoin(Resourcepart nickname) throws NoResponseException, XMPPErrorException, - SmackException, InterruptedException { + public synchronized MucCreateConfigFormHandle createOrJoin(Resourcepart nickname) throws NoResponseException, XMPPErrorException, + InterruptedException, MucAlreadyJoinedException, NotConnectedException { return createOrJoin(nickname, null, null, connection.getPacketReplyTimeout()); } @@ -402,16 +411,18 @@ public class MultiUserChat { * @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 milliseconds). - * @return true if the room creation was acknowledged by the service, false otherwise. + * @return A {@link MucCreateConfigFormHandle} if the room was created, or {code null} if the room was joined. * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if * the user is not allowed to create the room) * @throws NoResponseException if there was no response from the server. * @throws InterruptedException + * @throws MucAlreadyJoinedException if the MUC is already joined + * @throws NotConnectedException */ - public synchronized boolean createOrJoin(Resourcepart nickname, String password, DiscussionHistory history, long timeout) - throws NoResponseException, XMPPErrorException, SmackException, InterruptedException { + public synchronized MucCreateConfigFormHandle createOrJoin(Resourcepart nickname, String password, DiscussionHistory history, long timeout) + throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException { if (joined) { - throw new IllegalStateException("Creation failed - User already joined the room."); + throw new MucAlreadyJoinedException(); } Presence presence = enter(nickname, password, history, timeout); @@ -420,9 +431,78 @@ public class MultiUserChat { MUCUser mucUser = MUCUser.from(presence); if (mucUser != null && mucUser.getStatus().contains(Status.ROOM_CREATED_201)) { // Room was created and the user has joined the room - return true; + return new MucCreateConfigFormHandle(); + } + return null; + } + + + /** + * A handle used to configure a newly created room. As long as the room is not configured it will be locked, which + * means that no one is able to join. The room will become unlocked as soon it got configured. In order to create an + * instant room, use {@link #makeInstant()}. + *

+ * For advanced configuration options, use {@link MultiUserChat#getConfigurationForm()}, get the answer form with + * {@link Form#createAnswerForm()}, fill it out and send it back to the room with + * {@link MultiUserChat#sendConfigurationForm(Form)}. + *

+ */ + public class MucCreateConfigFormHandle { + + /** + * Create an instant room. The default configuration will be accepted and the room will become unlocked, i.e. + * other users are able to join. + * + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + * @see XEP-45 § 10.1.2 Creating an + * Instant Room + */ + public void makeInstant() throws NoResponseException, XMPPErrorException, NotConnectedException, + InterruptedException { + sendConfigurationForm(new Form(DataForm.Type.submit)); + } + + /** + * Alias for {@link MultiUserChat#getConfigFormManager()}. + * + * @return a MUC configuration form manager for this room. + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + * @see MultiUserChat#getConfigFormManager() + */ + public MucConfigFormManager getConfigFormManager() throws NoResponseException, + XMPPErrorException, NotConnectedException, InterruptedException { + return MultiUserChat.this.getConfigFormManager(); + } + } + + /** + * Create or join a MUC if it is necessary, i.e. if not the MUC is not already joined. + * + * @param nickname the required nickname to use. + * @param password the optional password required to join + * @return A {@link MucCreateConfigFormHandle} if the room was created, or {code null} if the room was joined. + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public MucCreateConfigFormHandle createOrJoinIfNecessary(Resourcepart nickname, String password) throws NoResponseException, + XMPPErrorException, NotConnectedException, InterruptedException { + if (isJoined()) { + return null; + } + try { + return createOrJoin(nickname, password, null, connection.getPacketReplyTimeout()); + } + catch (MucAlreadyJoinedException e) { + return null; } - return false; } /** @@ -466,10 +546,11 @@ public class MultiUserChat { * 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. - * @throws SmackException if there was no response from the server. * @throws InterruptedException + * @throws NotConnectedException + * @throws NoResponseException if there was no response from the server. */ - public void join(Resourcepart nickname, String password) throws XMPPErrorException, SmackException, InterruptedException { + public void join(Resourcepart nickname, String password) throws XMPPErrorException, InterruptedException, NoResponseException, NotConnectedException { join(nickname, password, null, connection.getPacketReplyTimeout()); } @@ -547,6 +628,25 @@ public class MultiUserChat { userHasLeft(); } + /** + * Get a {@link MucConfigFormManager} to configure this room. + *

+ * Only room owners are able to configure a room. + *

+ * + * @return a MUC configuration form manager for this room. + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + * @see XEP-45 § 10.2 Subsequent Room Configuration + * @since 4.2 + */ + public MucConfigFormManager getConfigFormManager() throws NoResponseException, + XMPPErrorException, NotConnectedException, InterruptedException { + return new MucConfigFormManager(this); + } + /** * Returns the room's configuration form that the room's owner can use or null if * no configuration is possible. The configuration form allows to set the room's language, @@ -570,14 +670,13 @@ public class MultiUserChat { /** * 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). + * with the new settings defined in the form. * * @param form the form with the new settings. * @throws XMPPErrorException if an error occurs setting the new rooms' configuration. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException - * @throws InterruptedException + * @throws InterruptedException */ public void sendConfigurationForm(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { MUCOwner iq = new MUCOwner(); @@ -868,13 +967,14 @@ public class MultiUserChat { * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException * @throws InterruptedException + * @throws MucNotJoinedException */ - public void changeNickname(Resourcepart nickname) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + public void changeNickname(Resourcepart nickname) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, MucNotJoinedException { StringUtils.requireNotNullOrEmpty(nickname, "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."); + throw new MucNotJoinedException(this); } final FullJid jid = JidCreate.fullFrom(room, nickname); // We change the nickname by sending a presence packet where the "to" @@ -905,14 +1005,14 @@ public class MultiUserChat { * @param mode the mode type for the presence update. * @throws NotConnectedException * @throws InterruptedException + * @throws MucNotJoinedException */ - public void changeAvailabilityStatus(String status, Presence.Mode mode) throws NotConnectedException, InterruptedException { + public void changeAvailabilityStatus(String status, Presence.Mode mode) throws NotConnectedException, InterruptedException, MucNotJoinedException { StringUtils.requireNotNullOrEmpty(nickname, "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."); + throw new MucNotJoinedException(this); } // We change the availability status by sending a presence packet to the room with the // new presence status and mode @@ -1689,11 +1789,11 @@ public class MultiUserChat { * * @return the next message if one is immediately available and * null otherwise. - * @throws MUCNotJoinedException + * @throws MucNotJoinedException */ - public Message pollMessage() throws MUCNotJoinedException { + public Message pollMessage() throws MucNotJoinedException { if (messageCollector == null) { - throw new MUCNotJoinedException(this); + throw new MucNotJoinedException(this); } return messageCollector.pollResult(); } @@ -1703,12 +1803,12 @@ public class MultiUserChat { * (not return) until a message is available. * * @return the next message. - * @throws MUCNotJoinedException + * @throws MucNotJoinedException * @throws InterruptedException */ - public Message nextMessage() throws MUCNotJoinedException, InterruptedException { + public Message nextMessage() throws MucNotJoinedException, InterruptedException { if (messageCollector == null) { - throw new MUCNotJoinedException(this); + throw new MucNotJoinedException(this); } return messageCollector.nextResult(); } @@ -1721,12 +1821,12 @@ public class MultiUserChat { * @param timeout the maximum amount of time to wait for the next message. * @return the next message, or null if the timeout elapses without a * message becoming available. - * @throws MUCNotJoinedException + * @throws MucNotJoinedException * @throws InterruptedException */ - public Message nextMessage(long timeout) throws MUCNotJoinedException, InterruptedException { + public Message nextMessage(long timeout) throws MucNotJoinedException, InterruptedException { if (messageCollector == null) { - throw new MUCNotJoinedException(this); + throw new MucNotJoinedException(this); } return messageCollector.nextResult(timeout); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatException.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatException.java new file mode 100644 index 000000000..5195b3ea4 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatException.java @@ -0,0 +1,86 @@ +/** + * + * Copyright © 2014-2015 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.muc; + +import org.jivesoftware.smack.SmackException; + +public abstract class MultiUserChatException extends SmackException { + + protected MultiUserChatException() { + } + + protected MultiUserChatException(String message) { + super(message); + } + + /** + * + */ + private static final long serialVersionUID = 1L; + + // This could eventually become an unchecked exception. But be aware that it's required in the + // control flow of MultiUserChat.createOrJoinIfNecessary + public static class MucAlreadyJoinedException extends MultiUserChatException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + } + + /** + * Thrown if the requested operation required the MUC to be joined by the + * client, while the client is currently joined. + * + */ + public static class MucNotJoinedException extends MultiUserChatException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public MucNotJoinedException(MultiUserChat multiUserChat) { + super("Client not currently joined " + multiUserChat.getRoom()); + } + } + + public static class MissingMucCreationAcknowledgeException extends MultiUserChatException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + } + + /** + * Thrown if the MUC room does not support the requested configuration option. + */ + public static class MucConfigurationNotSupported extends MultiUserChatException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public MucConfigurationNotSupported(String configString) { + super("The MUC configuration '" + configString + "' is not supported by the MUC service"); + } + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/bookmarkautojoin/MucBookmarkAutojoinManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/bookmarkautojoin/MucBookmarkAutojoinManager.java new file mode 100644 index 000000000..6d15de975 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/bookmarkautojoin/MucBookmarkAutojoinManager.java @@ -0,0 +1,145 @@ +/** + * + * Copyright © 2015 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.muc.bookmarkautojoin; + +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jivesoftware.smack.AbstractConnectionListener; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smackx.bookmarks.BookmarkManager; +import org.jivesoftware.smackx.bookmarks.BookmarkedConference; +import org.jivesoftware.smackx.muc.MultiUserChat; +import org.jivesoftware.smackx.muc.MultiUserChat.MucCreateConfigFormHandle; +import org.jivesoftware.smackx.muc.MultiUserChatManager; +import org.jxmpp.jid.parts.Resourcepart; + +/** + * Autojoin bookmarked Multi-User Chat conferences. + * + * @see XEP-48: Bookmarks + * + */ +public final class MucBookmarkAutojoinManager extends Manager { + + private static final Logger LOGGER = Logger.getLogger(MucBookmarkAutojoinManager.class.getName()); + + private static final Map INSTANCES = new WeakHashMap<>(); + + private static boolean autojoinEnabledDefault = false; + + static { + XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { + @Override + public void connectionCreated(XMPPConnection connection) { + getInstanceFor(connection); + } + }); + } + + public static void setAutojoinPerDefault(boolean autojoin) { + autojoinEnabledDefault = autojoin; + } + + public static synchronized MucBookmarkAutojoinManager getInstanceFor(XMPPConnection connection) { + MucBookmarkAutojoinManager mbam = INSTANCES.get(connection); + if (mbam == null) { + mbam = new MucBookmarkAutojoinManager(connection); + INSTANCES.put(connection, mbam); + } + return mbam; + } + + private final MultiUserChatManager multiUserChatManager; + private final BookmarkManager bookmarkManager; + + private boolean autojoinEnabled = autojoinEnabledDefault; + + private MucBookmarkAutojoinManager(XMPPConnection connection) { + super(connection); + multiUserChatManager = MultiUserChatManager.getInstanceFor(connection); + bookmarkManager = BookmarkManager.getBookmarkManager(connection); + connection.addConnectionListener(new AbstractConnectionListener() { + @Override + public void authenticated(XMPPConnection connection, boolean resumed) { + if (!autojoinEnabled) { + return; + } + // TODO handle resumed case? + autojoinBookmarkedConferences(); + } + }); + } + + public void setAutojoinEnabled(boolean autojoin) { + autojoinEnabled = autojoin; + } + + public void autojoinBookmarkedConferences() { + List bookmarkedConferences; + try { + bookmarkedConferences = bookmarkManager.getBookmarkedConferences(); + } + catch (NotConnectedException | InterruptedException e) { + LOGGER.log(Level.FINER, "Could not get MUC bookmarks", e); + return; + } + catch (NoResponseException | XMPPErrorException e) { + LOGGER.log(Level.WARNING, "Could not get MUC bookmarks", e); + return; + } + + final XMPPConnection connection = connection(); + Resourcepart defaultNick = connection.getUser().getResourcepart(); + + for (BookmarkedConference bookmarkedConference : bookmarkedConferences) { + if (!bookmarkedConference.isAutoJoin()) { + continue; + } + Resourcepart nick = bookmarkedConference.getNickname(); + if (nick == null) { + nick = defaultNick; + } + String password = bookmarkedConference.getPassword(); + MultiUserChat muc = multiUserChatManager.getMultiUserChat(bookmarkedConference.getJid()); + try { + MucCreateConfigFormHandle handle = muc.createOrJoinIfNecessary(nick, password); + if (handle != null) { + handle.makeInstant(); + } + } + catch (NotConnectedException | InterruptedException e) { + LOGGER.log(Level.FINER, "Could not autojoin bookmarked MUC", e); + // abort here + break; + } + catch (NoResponseException | XMPPErrorException e) { + // Do no abort, just log, + LOGGER.log(Level.WARNING, "Could not autojoin bookmarked MUC", e); + } + } + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MUCNotJoinedException.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/bookmarkautojoin/package-info.java similarity index 51% rename from smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MUCNotJoinedException.java rename to smack-extensions/src/main/java/org/jivesoftware/smackx/muc/bookmarkautojoin/package-info.java index 0b647546a..d69f5da6a 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MUCNotJoinedException.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/bookmarkautojoin/package-info.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright 2015 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,23 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.muc; - -import org.jivesoftware.smack.SmackException; /** - * Thrown if the requested operation required the MUC to be joined by the - * client, while the client is currently joined. - * + * Manager to autojoin bookmarked Multi-User Chat conferences. */ -public class MUCNotJoinedException extends SmackException { - - /** - * - */ - private static final long serialVersionUID = -5204934585663465576L; - - public MUCNotJoinedException(MultiUserChat multiUserChat) { - super("Client not currently joined " + multiUserChat.getRoom()); - } -} +package org.jivesoftware.smackx.muc.bookmarkautojoin; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/Form.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/Form.java index 58d8459d7..3f6d59a3c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/Form.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/xdata/Form.java @@ -353,6 +353,17 @@ public class Form { return dataForm.getField(variable); } + /** + * Check if a field with the given variable exists. + * + * @param variable the variable to check for. + * @return true if a field with the variable exists, false otherwise. + * @since 4.2 + */ + public boolean hasField(String variable) { + return dataForm.hasField(variable); + } + /** * Returns the instructions that explain how to fill out the form and what the form is about. * diff --git a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.xml b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.xml index b520a598e..fd878518a 100644 --- a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.xml +++ b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.xml @@ -3,6 +3,7 @@ org.jivesoftware.smackx.disco.ServiceDiscoveryManager org.jivesoftware.smackx.xhtmlim.XHTMLManager org.jivesoftware.smackx.muc.MultiUserChatManager + org.jivesoftware.smackx.muc.bookmarkautojoin.MucBookmarkAutojoinManager org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager org.jivesoftware.smackx.filetransfer.FileTransferManager diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java index a9ad6b655..39c0acae3 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java @@ -30,8 +30,7 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smackx.xdata.Form; -import org.jivesoftware.smackx.xdata.packet.DataForm; +import org.jivesoftware.smackx.muc.MultiUserChat.MucCreateConfigFormHandle; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.impl.JidCreate; @@ -82,9 +81,9 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { } }); - boolean newlyCreated = mucAsSeenByOne.createOrJoin(Resourcepart.from("one-" + randomString)); - if (newlyCreated) { - mucAsSeenByOne.sendConfigurationForm(new Form(DataForm.Type.submit)); + MucCreateConfigFormHandle handle = mucAsSeenByOne.createOrJoin(Resourcepart.from("one-" + randomString)); + if (handle != null) { + handle.makeInstant(); } mucAsSeenByTwo.join(Resourcepart.from("two-" + randomString)); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java new file mode 100644 index 000000000..95b1ba867 --- /dev/null +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java @@ -0,0 +1,91 @@ +/** + * + * Copyright 2015 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.muc; + + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.igniterealtime.smack.inttest.AbstractSmackLowLevelIntegrationTest; +import org.igniterealtime.smack.inttest.Configuration; +import org.igniterealtime.smack.inttest.SmackIntegrationTest; +import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.bookmarks.BookmarkManager; +import org.jivesoftware.smackx.muc.MultiUserChat.MucCreateConfigFormHandle; +import org.jivesoftware.smackx.muc.bookmarkautojoin.MucBookmarkAutojoinManager; +import org.jxmpp.jid.DomainBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.parts.Localpart; +import org.jxmpp.jid.parts.Resourcepart; + +public class MultiUserChatLowLevelIntegrationTest extends AbstractSmackLowLevelIntegrationTest { + + public MultiUserChatLowLevelIntegrationTest(Configuration configuration, String testRunId) throws Exception { + super(configuration, testRunId); + performCheck(new ConnectionCallback() { + @Override + public void connectionCallback(XMPPTCPConnection connection) throws Exception { + if (MultiUserChatManager.getInstanceFor(connection).getServiceNames().isEmpty()) { + throw new TestNotPossibleException("MUC component not offered by service"); + } + } + }); + } + + @SmackIntegrationTest + public void testMucBookmarksAutojoin(XMPPTCPConnection connection) throws InterruptedException, + TestNotPossibleException, XMPPException, SmackException, IOException { + final BookmarkManager bookmarkManager = BookmarkManager.getBookmarkManager(connection); + if (!bookmarkManager.isSupported()) { + throw new TestNotPossibleException("Private data storage not supported"); + } + final MultiUserChatManager multiUserChatManager = MultiUserChatManager.getInstanceFor(connection); + final Resourcepart mucNickname = Resourcepart.from("Nick-" + StringUtils.randomString(6)); + final String randomMucName = StringUtils.randomString(6); + final DomainBareJid mucComponent = multiUserChatManager.getServiceNames().get(0); + final MultiUserChat muc = multiUserChatManager.getMultiUserChat(JidCreate.bareFrom( + Localpart.from(randomMucName), mucComponent)); + + MucCreateConfigFormHandle handle = muc.createOrJoin(mucNickname); + if (handle != null) { + handle.makeInstant(); + } + muc.leave(); + + bookmarkManager.addBookmarkedConference("Smack Inttest: " + testRunId, muc.getRoom(), true, + mucNickname, null); + + connection.disconnect(); + connection.connect().login(); + + // MucBookmarkAutojoinManager is also able to do its task automatically + // after every login, it's not determinstic when this will be finished. + // So we trigger it manually here. + MucBookmarkAutojoinManager.getInstanceFor(connection).autojoinBookmarkedConferences(); + + assertTrue(muc.isJoined()); + + // If the test went well, leave the MUC + muc.leave(); + } + +} diff --git a/version.gradle b/version.gradle index b6659c768..ba09d576f 100644 --- a/version.gradle +++ b/version.gradle @@ -2,7 +2,7 @@ allprojects { ext { shortVersion = '4.2.0-alpha2' isSnapshot = true - jxmppVersion = '0.5.0-alpha2' + jxmppVersion = '0.5.0-alpha3' smackMinAndroidSdk = 8 } }