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