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