001/**
002 *
003 * Copyright 2016 Fernando Ramirez
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.muclight;
018
019import java.util.HashMap;
020import java.util.List;
021import java.util.Set;
022import java.util.concurrent.CopyOnWriteArraySet;
023
024import org.jivesoftware.smack.MessageListener;
025import org.jivesoftware.smack.SmackException.NoResponseException;
026import org.jivesoftware.smack.SmackException.NotConnectedException;
027import org.jivesoftware.smack.StanzaCollector;
028import org.jivesoftware.smack.StanzaListener;
029import org.jivesoftware.smack.XMPPConnection;
030import org.jivesoftware.smack.XMPPException.XMPPErrorException;
031import org.jivesoftware.smack.chat.ChatMessageListener;
032import org.jivesoftware.smack.filter.AndFilter;
033import org.jivesoftware.smack.filter.FromMatchesFilter;
034import org.jivesoftware.smack.filter.MessageTypeFilter;
035import org.jivesoftware.smack.filter.StanzaFilter;
036import org.jivesoftware.smack.packet.IQ;
037import org.jivesoftware.smack.packet.Message;
038import org.jivesoftware.smack.packet.Stanza;
039
040import org.jivesoftware.smackx.muclight.element.MUCLightAffiliationsIQ;
041import org.jivesoftware.smackx.muclight.element.MUCLightChangeAffiliationsIQ;
042import org.jivesoftware.smackx.muclight.element.MUCLightConfigurationIQ;
043import org.jivesoftware.smackx.muclight.element.MUCLightCreateIQ;
044import org.jivesoftware.smackx.muclight.element.MUCLightDestroyIQ;
045import org.jivesoftware.smackx.muclight.element.MUCLightGetAffiliationsIQ;
046import org.jivesoftware.smackx.muclight.element.MUCLightGetConfigsIQ;
047import org.jivesoftware.smackx.muclight.element.MUCLightGetInfoIQ;
048import org.jivesoftware.smackx.muclight.element.MUCLightInfoIQ;
049import org.jivesoftware.smackx.muclight.element.MUCLightSetConfigsIQ;
050
051import org.jxmpp.jid.EntityJid;
052import org.jxmpp.jid.Jid;
053
054/**
055 * MUCLight class.
056 * 
057 * @author Fernando Ramirez
058 */
059public class MultiUserChatLight {
060
061    public static final String NAMESPACE = "urn:xmpp:muclight:0";
062
063    public static final String AFFILIATIONS = "#affiliations";
064    public static final String INFO = "#info";
065    public static final String CONFIGURATION = "#configuration";
066    public static final String CREATE = "#create";
067    public static final String DESTROY = "#destroy";
068    public static final String BLOCKING = "#blocking";
069
070    private final XMPPConnection connection;
071    private final EntityJid room;
072
073    private final Set<MessageListener> messageListeners = new CopyOnWriteArraySet<MessageListener>();
074
075    /**
076     * This filter will match all stanzas send from the groupchat or from one if
077     * the groupchat occupants.
078     */
079    private final StanzaFilter fromRoomFilter;
080
081    /**
082     * Same as {@link #fromRoomFilter} together with
083     * {@link MessageTypeFilter#GROUPCHAT}.
084     */
085    private final StanzaFilter fromRoomGroupChatFilter;
086
087    private final StanzaListener messageListener;
088
089    private StanzaCollector messageCollector;
090
091    MultiUserChatLight(XMPPConnection connection, EntityJid room) {
092        this.connection = connection;
093        this.room = room;
094
095        fromRoomFilter = FromMatchesFilter.create(room);
096        fromRoomGroupChatFilter = new AndFilter(fromRoomFilter, MessageTypeFilter.GROUPCHAT);
097
098        messageListener = new StanzaListener() {
099            @Override
100            public void processStanza(Stanza packet) throws NotConnectedException {
101                Message message = (Message) packet;
102                for (MessageListener listener : messageListeners) {
103                    listener.processMessage(message);
104                }
105            }
106        };
107
108        connection.addSyncStanzaListener(messageListener, fromRoomGroupChatFilter);
109    }
110
111    /**
112     * Returns the JID of the room.
113     *
114     * @return the MUCLight room JID.
115     */
116    public EntityJid getRoom() {
117        return room;
118    }
119
120    /**
121     * Sends a message to the chat room.
122     *
123     * @param text
124     *            the text of the message to send.
125     * @throws NotConnectedException
126     * @throws InterruptedException
127     */
128    public void sendMessage(String text) throws NotConnectedException, InterruptedException {
129        Message message = createMessage();
130        message.setBody(text);
131        connection.sendStanza(message);
132    }
133
134    /**
135     * Returns a new Chat for sending private messages to a given room occupant.
136     * The Chat's occupant address is the room's JID (i.e.
137     * roomName@service/nick). The server service will change the 'from' address
138     * to the sender's room JID and delivering the message to the intended
139     * recipient's full JID.
140     *
141     * @param occupant
142     *            occupant unique room JID (e.g.
143     *            'darkcave@macbeth.shakespeare.lit/Paul').
144     * @param listener
145     *            the listener is a message listener that will handle messages
146     *            for the newly created chat.
147     * @return new Chat for sending private messages to a given room occupant.
148     */
149    @Deprecated
150    // Do not re-use Chat API, which was designed for XMPP-IM 1:1 chats and not MUClight private chats.
151    public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityJid occupant, ChatMessageListener listener) {
152        return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener);
153    }
154
155    /**
156     * Creates a new Message to send to the chat room.
157     *
158     * @return a new Message addressed to the chat room.
159     */
160    public Message createMessage() {
161        return new Message(room, Message.Type.groupchat);
162    }
163
164    /**
165     * Sends a Message to the chat room.
166     *
167     * @param message
168     *            the message.
169     * @throws NotConnectedException
170     * @throws InterruptedException
171     */
172    public void sendMessage(Message message) throws NotConnectedException, InterruptedException {
173        message.setTo(room);
174        message.setType(Message.Type.groupchat);
175        connection.sendStanza(message);
176    }
177
178    /**
179     * Polls for and returns the next message.
180     *
181     * @return the next message if one is immediately available
182     */
183    public Message pollMessage() {
184        return messageCollector.pollResult();
185    }
186
187    /**
188     * Returns the next available message in the chat. The method call will
189     * block (not return) until a message is available.
190     *
191     * @return the next message.
192     * @throws InterruptedException
193     */
194    public Message nextMessage() throws InterruptedException {
195        return messageCollector.nextResult();
196    }
197
198    /**
199     * Returns the next available message in the chat.
200     *
201     * @param timeout
202     *            the maximum amount of time to wait for the next message.
203     * @return the next message, or null if the timeout elapses without a
204     *         message becoming available.
205     * @throws InterruptedException
206     */
207    public Message nextMessage(long timeout) throws InterruptedException {
208        return messageCollector.nextResult(timeout);
209    }
210
211    /**
212     * Adds a stanza(/packet) listener that will be notified of any new messages
213     * in the group chat. Only "group chat" messages addressed to this group
214     * chat will be delivered to the listener.
215     *
216     * @param listener
217     *            a stanza(/packet) listener.
218     * @return true if the listener was not already added.
219     */
220    public boolean addMessageListener(MessageListener listener) {
221        return messageListeners.add(listener);
222    }
223
224    /**
225     * Removes a stanza(/packet) listener that was being notified of any new
226     * messages in the MUCLight. Only "group chat" messages addressed to this
227     * MUCLight were being delivered to the listener.
228     *
229     * @param listener
230     *            a stanza(/packet) listener.
231     * @return true if the listener was removed, otherwise the listener was not
232     *         added previously.
233     */
234    public boolean removeMessageListener(MessageListener listener) {
235        return messageListeners.remove(listener);
236    }
237
238    /**
239     * Remove the connection callbacks used by this MUC Light from the
240     * connection.
241     */
242    private void removeConnectionCallbacks() {
243        connection.removeSyncStanzaListener(messageListener);
244        if (messageCollector != null) {
245            messageCollector.cancel();
246            messageCollector = null;
247        }
248    }
249
250    @Override
251    public String toString() {
252        return "MUC Light: " + room + "(" + connection.getUser() + ")";
253    }
254
255    /**
256     * Create new MUCLight.
257     * 
258     * @param roomName
259     * @param subject
260     * @param customConfigs
261     * @param occupants
262     * @throws Exception
263     */
264    public void create(String roomName, String subject, HashMap<String, String> customConfigs, List<Jid> occupants)
265            throws Exception {
266        MUCLightCreateIQ createMUCLightIQ = new MUCLightCreateIQ(room, roomName, occupants);
267
268        messageCollector = connection.createStanzaCollector(fromRoomGroupChatFilter);
269
270        try {
271            connection.createStanzaCollectorAndSend(createMUCLightIQ).nextResultOrThrow();
272        } catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) {
273            removeConnectionCallbacks();
274            throw e;
275        }
276    }
277
278    /**
279     * Create new MUCLight.
280     * 
281     * @param roomName
282     * @param occupants
283     * @throws Exception
284     */
285    public void create(String roomName, List<Jid> occupants) throws Exception {
286        create(roomName, null, null, occupants);
287    }
288
289    /**
290     * Leave the MUCLight.
291     * 
292     * @throws NotConnectedException
293     * @throws InterruptedException
294     * @throws NoResponseException
295     * @throws XMPPErrorException
296     */
297    public void leave() throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException {
298        HashMap<Jid, MUCLightAffiliation> affiliations = new HashMap<>();
299        affiliations.put(connection.getUser(), MUCLightAffiliation.none);
300
301        MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations);
302        IQ responseIq = connection.createStanzaCollectorAndSend(changeAffiliationsIQ).nextResultOrThrow();
303        boolean roomLeft = responseIq.getType().equals(IQ.Type.result);
304
305        if (roomLeft) {
306            removeConnectionCallbacks();
307        }
308    }
309
310    /**
311     * Get the MUC Light info.
312     * 
313     * @param version
314     * @return the room info
315     * @throws NoResponseException
316     * @throws XMPPErrorException
317     * @throws NotConnectedException
318     * @throws InterruptedException
319     */
320    public MUCLightRoomInfo getFullInfo(String version)
321            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
322        MUCLightGetInfoIQ mucLightGetInfoIQ = new MUCLightGetInfoIQ(room, version);
323
324        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetInfoIQ).nextResultOrThrow();
325        MUCLightInfoIQ mucLightInfoResponseIQ = (MUCLightInfoIQ) responseIq;
326
327        return new MUCLightRoomInfo(mucLightInfoResponseIQ.getVersion(), room,
328                mucLightInfoResponseIQ.getConfiguration(), mucLightInfoResponseIQ.getOccupants());
329    }
330
331    /**
332     * Get the MUC Light info.
333     * 
334     * @return the room info
335     * @throws NoResponseException
336     * @throws XMPPErrorException
337     * @throws NotConnectedException
338     * @throws InterruptedException
339     */
340    public MUCLightRoomInfo getFullInfo()
341            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
342        return getFullInfo(null);
343    }
344
345    /**
346     * Get the MUC Light configuration.
347     * 
348     * @param version
349     * @return the room configuration
350     * @throws NoResponseException
351     * @throws XMPPErrorException
352     * @throws NotConnectedException
353     * @throws InterruptedException
354     */
355    public MUCLightRoomConfiguration getConfiguration(String version)
356            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
357        MUCLightGetConfigsIQ mucLightGetConfigsIQ = new MUCLightGetConfigsIQ(room, version);
358        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetConfigsIQ).nextResultOrThrow();
359        MUCLightConfigurationIQ mucLightConfigurationIQ = (MUCLightConfigurationIQ) responseIq;
360        return mucLightConfigurationIQ.getConfiguration();
361    }
362
363    /**
364     * Get the MUC Light configuration.
365     * 
366     * @return the room configuration
367     * @throws NoResponseException
368     * @throws XMPPErrorException
369     * @throws NotConnectedException
370     * @throws InterruptedException
371     */
372    public MUCLightRoomConfiguration getConfiguration()
373            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
374        return getConfiguration(null);
375    }
376
377    /**
378     * Get the MUC Light affiliations.
379     * 
380     * @param version
381     * @return the room affiliations
382     * @throws NoResponseException
383     * @throws XMPPErrorException
384     * @throws NotConnectedException
385     * @throws InterruptedException
386     */
387    public HashMap<Jid, MUCLightAffiliation> getAffiliations(String version)
388            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
389        MUCLightGetAffiliationsIQ mucLightGetAffiliationsIQ = new MUCLightGetAffiliationsIQ(room, version);
390
391        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetAffiliationsIQ).nextResultOrThrow();
392        MUCLightAffiliationsIQ mucLightAffiliationsIQ = (MUCLightAffiliationsIQ) responseIq;
393
394        return mucLightAffiliationsIQ.getAffiliations();
395    }
396
397    /**
398     * Get the MUC Light affiliations.
399     * 
400     * @return the room affiliations
401     * @throws NoResponseException
402     * @throws XMPPErrorException
403     * @throws NotConnectedException
404     * @throws InterruptedException
405     */
406    public HashMap<Jid, MUCLightAffiliation> getAffiliations()
407            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
408        return getAffiliations(null);
409    }
410
411    /**
412     * Change the MUC Light affiliations.
413     * 
414     * @param affiliations
415     * @throws NoResponseException
416     * @throws XMPPErrorException
417     * @throws NotConnectedException
418     * @throws InterruptedException
419     */
420    public void changeAffiliations(HashMap<Jid, MUCLightAffiliation> affiliations)
421            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
422        MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations);
423        connection.createStanzaCollectorAndSend(changeAffiliationsIQ).nextResultOrThrow();
424    }
425
426    /**
427     * Destroy the MUC Light. Only will work if it is requested by the owner.
428     * 
429     * @throws NoResponseException
430     * @throws XMPPErrorException
431     * @throws NotConnectedException
432     * @throws InterruptedException
433     */
434    public void destroy() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
435        MUCLightDestroyIQ mucLightDestroyIQ = new MUCLightDestroyIQ(room);
436        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightDestroyIQ).nextResultOrThrow();
437        boolean roomDestroyed = responseIq.getType().equals(IQ.Type.result);
438
439        if (roomDestroyed) {
440            removeConnectionCallbacks();
441        }
442    }
443
444    /**
445     * Change the subject of the MUC Light.
446     * 
447     * @param subject
448     * @throws NoResponseException
449     * @throws XMPPErrorException
450     * @throws NotConnectedException
451     * @throws InterruptedException
452     */
453    public void changeSubject(String subject)
454            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
455        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, null, subject, null);
456        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
457    }
458
459    /**
460     * Change the name of the room.
461     * 
462     * @param roomName
463     * @throws NoResponseException
464     * @throws XMPPErrorException
465     * @throws NotConnectedException
466     * @throws InterruptedException
467     */
468    public void changeRoomName(String roomName)
469            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
470        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, null);
471        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
472    }
473
474    /**
475     * Set the room configurations.
476     * 
477     * @param customConfigs
478     * @throws NoResponseException
479     * @throws XMPPErrorException
480     * @throws NotConnectedException
481     * @throws InterruptedException
482     */
483    public void setRoomConfigs(HashMap<String, String> customConfigs)
484            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
485        setRoomConfigs(null, customConfigs);
486    }
487
488    /**
489     * Set the room configurations.
490     * 
491     * @param roomName
492     * @param customConfigs
493     * @throws NoResponseException
494     * @throws XMPPErrorException
495     * @throws NotConnectedException
496     * @throws InterruptedException
497     */
498    public void setRoomConfigs(String roomName, HashMap<String, String> customConfigs)
499            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
500        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, customConfigs);
501        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
502    }
503
504}