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    @SuppressWarnings("deprecation")
150    @Deprecated
151    // Do not re-use Chat API, which was designed for XMPP-IM 1:1 chats and not MUClight private chats.
152    public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityJid occupant, ChatMessageListener listener) {
153        return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener);
154    }
155
156    /**
157     * Creates a new Message to send to the chat room.
158     *
159     * @return a new Message addressed to the chat room.
160     */
161    public Message createMessage() {
162        return new Message(room, Message.Type.groupchat);
163    }
164
165    /**
166     * Sends a Message to the chat room.
167     *
168     * @param message
169     *            the message.
170     * @throws NotConnectedException
171     * @throws InterruptedException
172     */
173    public void sendMessage(Message message) throws NotConnectedException, InterruptedException {
174        message.setTo(room);
175        message.setType(Message.Type.groupchat);
176        connection.sendStanza(message);
177    }
178
179    /**
180     * Polls for and returns the next message.
181     *
182     * @return the next message if one is immediately available
183     */
184    public Message pollMessage() {
185        return messageCollector.pollResult();
186    }
187
188    /**
189     * Returns the next available message in the chat. The method call will
190     * block (not return) until a message is available.
191     *
192     * @return the next message.
193     * @throws InterruptedException
194     */
195    public Message nextMessage() throws InterruptedException {
196        return messageCollector.nextResult();
197    }
198
199    /**
200     * Returns the next available message in the chat.
201     *
202     * @param timeout
203     *            the maximum amount of time to wait for the next message.
204     * @return the next message, or null if the timeout elapses without a
205     *         message becoming available.
206     * @throws InterruptedException
207     */
208    public Message nextMessage(long timeout) throws InterruptedException {
209        return messageCollector.nextResult(timeout);
210    }
211
212    /**
213     * Adds a stanza listener that will be notified of any new messages
214     * in the group chat. Only "group chat" messages addressed to this group
215     * chat will be delivered to the listener.
216     *
217     * @param listener
218     *            a stanza listener.
219     * @return true if the listener was not already added.
220     */
221    public boolean addMessageListener(MessageListener listener) {
222        return messageListeners.add(listener);
223    }
224
225    /**
226     * Removes a stanza listener that was being notified of any new
227     * messages in the MUCLight. Only "group chat" messages addressed to this
228     * MUCLight were being delivered to the listener.
229     *
230     * @param listener
231     *            a stanza listener.
232     * @return true if the listener was removed, otherwise the listener was not
233     *         added previously.
234     */
235    public boolean removeMessageListener(MessageListener listener) {
236        return messageListeners.remove(listener);
237    }
238
239    /**
240     * Remove the connection callbacks used by this MUC Light from the
241     * connection.
242     */
243    private void removeConnectionCallbacks() {
244        connection.removeSyncStanzaListener(messageListener);
245        if (messageCollector != null) {
246            messageCollector.cancel();
247            messageCollector = null;
248        }
249    }
250
251    @Override
252    public String toString() {
253        return "MUC Light: " + room + "(" + connection.getUser() + ")";
254    }
255
256    /**
257     * Create new MUCLight.
258     *
259     * @param roomName
260     * @param subject
261     * @param customConfigs
262     * @param occupants
263     * @throws Exception
264     */
265    public void create(String roomName, String subject, HashMap<String, String> customConfigs, List<Jid> occupants)
266            throws Exception {
267        MUCLightCreateIQ createMUCLightIQ = new MUCLightCreateIQ(room, roomName, occupants);
268
269        messageCollector = connection.createStanzaCollector(fromRoomGroupChatFilter);
270
271        try {
272            connection.createStanzaCollectorAndSend(createMUCLightIQ).nextResultOrThrow();
273        } catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) {
274            removeConnectionCallbacks();
275            throw e;
276        }
277    }
278
279    /**
280     * Create new MUCLight.
281     *
282     * @param roomName
283     * @param occupants
284     * @throws Exception
285     */
286    public void create(String roomName, List<Jid> occupants) throws Exception {
287        create(roomName, null, null, occupants);
288    }
289
290    /**
291     * Leave the MUCLight.
292     *
293     * @throws NotConnectedException
294     * @throws InterruptedException
295     * @throws NoResponseException
296     * @throws XMPPErrorException
297     */
298    public void leave() throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException {
299        HashMap<Jid, MUCLightAffiliation> affiliations = new HashMap<>();
300        affiliations.put(connection.getUser(), MUCLightAffiliation.none);
301
302        MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations);
303        IQ responseIq = connection.createStanzaCollectorAndSend(changeAffiliationsIQ).nextResultOrThrow();
304        boolean roomLeft = responseIq.getType().equals(IQ.Type.result);
305
306        if (roomLeft) {
307            removeConnectionCallbacks();
308        }
309    }
310
311    /**
312     * Get the MUC Light info.
313     *
314     * @param version
315     * @return the room info
316     * @throws NoResponseException
317     * @throws XMPPErrorException
318     * @throws NotConnectedException
319     * @throws InterruptedException
320     */
321    public MUCLightRoomInfo getFullInfo(String version)
322            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
323        MUCLightGetInfoIQ mucLightGetInfoIQ = new MUCLightGetInfoIQ(room, version);
324
325        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetInfoIQ).nextResultOrThrow();
326        MUCLightInfoIQ mucLightInfoResponseIQ = (MUCLightInfoIQ) responseIq;
327
328        return new MUCLightRoomInfo(mucLightInfoResponseIQ.getVersion(), room,
329                mucLightInfoResponseIQ.getConfiguration(), mucLightInfoResponseIQ.getOccupants());
330    }
331
332    /**
333     * Get the MUC Light info.
334     *
335     * @return the room info
336     * @throws NoResponseException
337     * @throws XMPPErrorException
338     * @throws NotConnectedException
339     * @throws InterruptedException
340     */
341    public MUCLightRoomInfo getFullInfo()
342            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
343        return getFullInfo(null);
344    }
345
346    /**
347     * Get the MUC Light configuration.
348     *
349     * @param version
350     * @return the room configuration
351     * @throws NoResponseException
352     * @throws XMPPErrorException
353     * @throws NotConnectedException
354     * @throws InterruptedException
355     */
356    public MUCLightRoomConfiguration getConfiguration(String version)
357            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
358        MUCLightGetConfigsIQ mucLightGetConfigsIQ = new MUCLightGetConfigsIQ(room, version);
359        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetConfigsIQ).nextResultOrThrow();
360        MUCLightConfigurationIQ mucLightConfigurationIQ = (MUCLightConfigurationIQ) responseIq;
361        return mucLightConfigurationIQ.getConfiguration();
362    }
363
364    /**
365     * Get the MUC Light configuration.
366     *
367     * @return the room configuration
368     * @throws NoResponseException
369     * @throws XMPPErrorException
370     * @throws NotConnectedException
371     * @throws InterruptedException
372     */
373    public MUCLightRoomConfiguration getConfiguration()
374            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
375        return getConfiguration(null);
376    }
377
378    /**
379     * Get the MUC Light affiliations.
380     *
381     * @param version
382     * @return the room affiliations
383     * @throws NoResponseException
384     * @throws XMPPErrorException
385     * @throws NotConnectedException
386     * @throws InterruptedException
387     */
388    public HashMap<Jid, MUCLightAffiliation> getAffiliations(String version)
389            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
390        MUCLightGetAffiliationsIQ mucLightGetAffiliationsIQ = new MUCLightGetAffiliationsIQ(room, version);
391
392        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightGetAffiliationsIQ).nextResultOrThrow();
393        MUCLightAffiliationsIQ mucLightAffiliationsIQ = (MUCLightAffiliationsIQ) responseIq;
394
395        return mucLightAffiliationsIQ.getAffiliations();
396    }
397
398    /**
399     * Get the MUC Light affiliations.
400     *
401     * @return the room affiliations
402     * @throws NoResponseException
403     * @throws XMPPErrorException
404     * @throws NotConnectedException
405     * @throws InterruptedException
406     */
407    public HashMap<Jid, MUCLightAffiliation> getAffiliations()
408            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
409        return getAffiliations(null);
410    }
411
412    /**
413     * Change the MUC Light affiliations.
414     *
415     * @param affiliations
416     * @throws NoResponseException
417     * @throws XMPPErrorException
418     * @throws NotConnectedException
419     * @throws InterruptedException
420     */
421    public void changeAffiliations(HashMap<Jid, MUCLightAffiliation> affiliations)
422            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
423        MUCLightChangeAffiliationsIQ changeAffiliationsIQ = new MUCLightChangeAffiliationsIQ(room, affiliations);
424        connection.createStanzaCollectorAndSend(changeAffiliationsIQ).nextResultOrThrow();
425    }
426
427    /**
428     * Destroy the MUC Light. Only will work if it is requested by the owner.
429     *
430     * @throws NoResponseException
431     * @throws XMPPErrorException
432     * @throws NotConnectedException
433     * @throws InterruptedException
434     */
435    public void destroy() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
436        MUCLightDestroyIQ mucLightDestroyIQ = new MUCLightDestroyIQ(room);
437        IQ responseIq = connection.createStanzaCollectorAndSend(mucLightDestroyIQ).nextResultOrThrow();
438        boolean roomDestroyed = responseIq.getType().equals(IQ.Type.result);
439
440        if (roomDestroyed) {
441            removeConnectionCallbacks();
442        }
443    }
444
445    /**
446     * Change the subject of the MUC Light.
447     *
448     * @param subject
449     * @throws NoResponseException
450     * @throws XMPPErrorException
451     * @throws NotConnectedException
452     * @throws InterruptedException
453     */
454    public void changeSubject(String subject)
455            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
456        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, null, subject, null);
457        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
458    }
459
460    /**
461     * Change the name of the room.
462     *
463     * @param roomName
464     * @throws NoResponseException
465     * @throws XMPPErrorException
466     * @throws NotConnectedException
467     * @throws InterruptedException
468     */
469    public void changeRoomName(String roomName)
470            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
471        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, null);
472        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
473    }
474
475    /**
476     * Set the room configurations.
477     *
478     * @param customConfigs
479     * @throws NoResponseException
480     * @throws XMPPErrorException
481     * @throws NotConnectedException
482     * @throws InterruptedException
483     */
484    public void setRoomConfigs(HashMap<String, String> customConfigs)
485            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
486        setRoomConfigs(null, customConfigs);
487    }
488
489    /**
490     * Set the room configurations.
491     *
492     * @param roomName
493     * @param customConfigs
494     * @throws NoResponseException
495     * @throws XMPPErrorException
496     * @throws NotConnectedException
497     * @throws InterruptedException
498     */
499    public void setRoomConfigs(String roomName, HashMap<String, String> customConfigs)
500            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
501        MUCLightSetConfigsIQ mucLightSetConfigIQ = new MUCLightSetConfigsIQ(room, roomName, customConfigs);
502        connection.createStanzaCollectorAndSend(mucLightSetConfigIQ).nextResultOrThrow();
503    }
504
505}