1 /*
2  * Copyright (C) 2005-2008 Jive Software. All rights reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jivesoftware.openfire.muc;
17 
18 import com.google.common.collect.Multimap;
19 import org.jivesoftware.database.DbConnectionManager;
20 import org.jivesoftware.database.SequenceManager;
21 import org.jivesoftware.openfire.XMPPServer;
22 import org.jivesoftware.openfire.cluster.ClusterEventListener;
23 import org.jivesoftware.openfire.cluster.ClusterManager;
24 import org.jivesoftware.openfire.container.BasicModule;
25 import org.jivesoftware.openfire.event.UserEventDispatcher;
26 import org.jivesoftware.openfire.event.UserEventListener;
27 import org.jivesoftware.openfire.muc.cluster.ServiceAddedEvent;
28 import org.jivesoftware.openfire.muc.cluster.ServiceRemovedEvent;
29 import org.jivesoftware.openfire.muc.cluster.ServiceUpdatedEvent;
30 import org.jivesoftware.openfire.muc.spi.MUCPersistenceManager;
31 import org.jivesoftware.openfire.muc.spi.MUCServicePropertyEventDispatcher;
32 import org.jivesoftware.openfire.muc.spi.MUCServicePropertyEventListener;
33 import org.jivesoftware.openfire.muc.spi.MultiUserChatServiceImpl;
34 import org.jivesoftware.openfire.stats.Statistic;
35 import org.jivesoftware.openfire.stats.StatisticsManager;
36 import org.jivesoftware.openfire.user.User;
37 import org.jivesoftware.util.AlreadyExistsException;
38 import org.jivesoftware.util.JiveConstants;
39 import org.jivesoftware.util.LocaleUtils;
40 import org.jivesoftware.util.NotFoundException;
41 import org.jivesoftware.util.cache.CacheFactory;
42 import org.jivesoftware.util.cache.ConsistencyChecks;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45 import org.xmpp.component.ComponentException;
46 import org.xmpp.component.ComponentManagerFactory;
47 import org.xmpp.packet.JID;
48 
49 import javax.annotation.Nonnull;
50 import javax.annotation.Nullable;
51 import java.sql.Connection;
52 import java.sql.PreparedStatement;
53 import java.sql.ResultSet;
54 import java.sql.SQLException;
55 import java.sql.Types;
56 import java.util.ArrayList;
57 import java.util.Comparator;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.concurrent.ConcurrentHashMap;
61 import java.util.stream.Collectors;
62 
63 /**
64  * Provides centralized management of all configured Multi User Chat (MUC) services.
65  *
66  * @author Daniel Henninger
67  */
68 public class MultiUserChatManager extends BasicModule implements MUCServicePropertyEventListener,
69         UserEventListener {
70 
71     private static final Logger Log = LoggerFactory.getLogger(MultiUserChatManager.class);
72 
73     private static final String LOAD_SERVICES = "SELECT subdomain,description,isHidden FROM ofMucService";
74     private static final String LOAD_SERVICE = "SELECT description,isHidden FROM ofMucService WHERE subdomain =?";
75     private static final String CREATE_SERVICE = "INSERT INTO ofMucService(serviceID,subdomain,description,isHidden) VALUES(?,?,?,?)";
76     private static final String UPDATE_SERVICE = "UPDATE ofMucService SET subdomain=?,description=? WHERE serviceID=?";
77     private static final String DELETE_SERVICE = "DELETE FROM ofMucService WHERE serviceID=?";
78     private static final String LOAD_SERVICE_ID = "SELECT serviceID FROM ofMucService WHERE subdomain=?";
79     private static final String LOAD_SUBDOMAIN = "SELECT subdomain FROM ofMucService WHERE serviceID=?";
80 
81     /**
82      * Statistics keys
83      */
84     private static final String roomsStatKey = "muc_rooms";
85     private static final String occupantsStatKey = "muc_occupants";
86     private static final String usersStatKey = "muc_users";
87     private static final String incomingStatKey = "muc_incoming";
88     private static final String outgoingStatKey = "muc_outgoing";
89     private static final String trafficStatGroup = "muc_traffic";
90 
91     private final ConcurrentHashMap<String,MultiUserChatService> mucServices = new ConcurrentHashMap<>();
92 
93     /**
94      * Creates a new MultiUserChatManager instance.
95      */
MultiUserChatManager()96     public MultiUserChatManager() {
97         super("Multi user chat manager");
98     }
99 
100     /**
101      * Called when manager starts up, to initialize things.
102      */
103     @Override
start()104     public void start() {
105         super.start();
106 
107         loadServices();
108 
109         for (MultiUserChatService service : mucServices.values()) {
110             registerMultiUserChatService(service, false);
111         }
112 
113         // Add statistics
114         addTotalRoomStats();
115         addTotalOccupantsStats();
116         addTotalConnectedUsers();
117         addNumberIncomingMessages();
118         addNumberOutgoingMessages();
119 
120         UserEventDispatcher.addListener(this);
121         MUCServicePropertyEventDispatcher.addListener(this);
122     }
123 
124     /**
125      * Called when manager is stopped, to clean things up.
126      */
127     @Override
stop()128     public void stop() {
129         super.stop();
130 
131         UserEventDispatcher.removeListener(this);
132         MUCServicePropertyEventDispatcher.removeListener(this);
133 
134         // Remove the statistics.
135         StatisticsManager.getInstance().removeStatistic(roomsStatKey);
136         StatisticsManager.getInstance().removeStatistic(occupantsStatKey);
137         StatisticsManager.getInstance().removeStatistic(usersStatKey);
138         StatisticsManager.getInstance().removeStatistic(incomingStatKey);
139         StatisticsManager.getInstance().removeStatistic(outgoingStatKey);
140 
141         for (MultiUserChatService service : mucServices.values()) {
142             unregisterMultiUserChatService(service.getServiceName(), false);
143         }
144     }
145 
146     /**
147      * Registers a new MultiUserChatService implementation to the manager.
148      *
149      * This is typically used if you have a custom MUC implementation that you want to register with the manager. In
150      * other words, it may not be database stored and may follow special rules, implementing MultiUserChatService.
151      * It is also used internally to register services from the database.
152      *
153      * Triggers the service to start up.
154      *
155      * An event will be sent to all other cluster nodes to inform them that a new service was added.
156      *
157      * @param service The MultiUserChatService to be registered.
158      * @see #createMultiUserChatService(String, String, boolean)
159      */
registerMultiUserChatService(@onnull final MultiUserChatService service)160     public void registerMultiUserChatService(@Nonnull final MultiUserChatService service) {
161         registerMultiUserChatService(service, true);
162     }
163 
164     /**
165      * Registers a new MultiUserChatService implementation to the manager.
166      *
167      * This is typically used if you have a custom MUC implementation that you want to register with the manager. In
168      * other words, it may not be database stored and may follow special rules, implementing MultiUserChatService.
169      * It is also used internally to register services from the database.
170      *
171      * Triggers the service to start up.
172      *
173      * This method has a boolean parameter that controls whether a 'new service added' event is to be sent to all other
174      * cluster nodes. This generally is desirable when a new service is being created. A reason to _not_ send such an
175      * event is when this method is being invoked as a result of receiving/processing such an event that was received
176      * from another cluster node, or when initializing this instance from database content (which will occur on all
177      * cluster nodes).
178      *
179      * @param service The MultiUserChatService to be registered.
180      * @param allNodes true if a 'service added' event needs to be sent to other cluster nodes.
181      * @see #createMultiUserChatService(String, String, boolean)
182      */
registerMultiUserChatService(@onnull final MultiUserChatService service, final boolean allNodes)183     public void registerMultiUserChatService(@Nonnull final MultiUserChatService service, final boolean allNodes) {
184         Log.debug("Registering MUC service '{}'", service.getServiceName());
185         try {
186             ComponentManagerFactory.getComponentManager().addComponent(service.getServiceName(), service);
187             mucServices.put(service.getServiceName(), service);
188         }
189         catch (ComponentException e) {
190             Log.error("Unable to register MUC service '{}' as a component.", service.getServiceName(), e);
191         }
192         if (allNodes) {
193             Log.trace("Sending 'service added' event for MUC service '{}' to all other cluster nodes.", service.getServiceName());
194             CacheFactory.doClusterTask(new ServiceAddedEvent(service.getServiceName(), service.getDescription(), service.isHidden()));
195         }
196     }
197 
198     /**
199      * Unregisters a MultiUserChatService from the manager.
200      *
201      * It can be used to explicitly unregister services, and is also used internally to unregister database stored services.
202      *
203      * Triggers the service to shut down.
204      *
205      * An event will be sent to all other cluster nodes to inform them that a new service was added.
206      *
207      * @param subdomain The subdomain of the service to be unregistered.
208      * @see #removeMultiUserChatService(String)
209      */
unregisterMultiUserChatService(@onnull final String subdomain)210     public void unregisterMultiUserChatService(@Nonnull final String subdomain) {
211         unregisterMultiUserChatService(subdomain, true);
212     }
213 
214     /**
215      * Unregisters a MultiUserChatService from the manager.
216      *
217      * It can be used to explicitly unregister services, and is also used internally to unregister database stored services.
218      *
219      * Triggers the service to shut down.
220      *
221      * This method has a boolean parameter that controls whether a 'service removed' event is to be sent to all other
222      * cluster nodes. This generally is desirable when a pre-existing service is being removed. A reason to _not_ send
223      * such an event is when this method is being invoked as a result of receiving/processing such an event that was
224      * received from another cluster node, or when shutting down this instance (as the service might continue to live on
225      * other cluster nodes).
226      *
227      * @param subdomain The subdomain of the service to be unregistered.
228      * @param allNodes true if a 'service removed' event needs to be sent to other cluster nodes.
229      * @see #removeMultiUserChatService(String)
230      */
unregisterMultiUserChatService(@onnull final String subdomain, final boolean allNodes)231     public void unregisterMultiUserChatService(@Nonnull final String subdomain, final boolean allNodes) {
232         Log.debug("Unregistering MUC service '{}'", subdomain);
233         final MultiUserChatService service = mucServices.remove(subdomain);
234         if (service != null) {
235             service.shutdown();
236             try {
237                 ComponentManagerFactory.getComponentManager().removeComponent(subdomain);
238             }
239             catch (ComponentException e) {
240                 Log.error("Unable to remove MUC service '{}' from component manager.", subdomain, e);
241                 mucServices.put(subdomain, service);
242             }
243         }
244         if (allNodes) {
245             Log.trace("Sending 'service removed' event for MUC service '{}' to all other cluster nodes.", subdomain);
246             CacheFactory.doClusterTask(new ServiceRemovedEvent(subdomain));
247         }
248     }
249 
250     /**
251      * Returns the number of registered MultiUserChatServices.
252      *
253      * @param includePrivate True if you want to include private/hidden services in the count.
254      * @return Number of registered services.
255      */
getServicesCount(final boolean includePrivate)256     public int getServicesCount(final boolean includePrivate) {
257         int servicesCnt = mucServices.size();
258         if (!includePrivate) {
259             for (MultiUserChatService service : mucServices.values()) {
260                 if (service.isHidden()) {
261                     servicesCnt--;
262                 }
263             }
264         }
265         return servicesCnt;
266     }
267 
268     /**
269      * Creates a new MUC service and registers it with the manager (which causes a cluster-wide notification to be sent)
270      * and starts up the service.
271      *
272      * @param subdomain Subdomain of the MUC service.
273      * @param description Description of the MUC service (can be null for default description)
274      * @param isHidden True if the service is hidden from view in services lists.
275      * @return MultiUserChatService implementation that was just created.
276      * @throws AlreadyExistsException if the service already exists.
277      */
278     @Nonnull
createMultiUserChatService(@onnull final String subdomain, @Nullable final String description, final boolean isHidden)279     public MultiUserChatServiceImpl createMultiUserChatService(@Nonnull final String subdomain, @Nullable final String description, final boolean isHidden) throws AlreadyExistsException {
280         if (getMultiUserChatServiceID(subdomain) != null) {
281             Log.info("Unable to create a service for {} as one already exists.", subdomain);
282             throw new AlreadyExistsException();
283         }
284 
285         Log.info("Creating MUC service '{}'", subdomain);
286         final MultiUserChatServiceImpl muc = new MultiUserChatServiceImpl(subdomain, description, isHidden);
287         insertService(subdomain, description, isHidden);
288         registerMultiUserChatService(muc);
289         return muc;
290     }
291 
292     /**
293      * Updates the configuration of a MUC service.
294      *
295      * This is more involved than it may seem.
296      *
297      * If the subdomain is changed, we need to shut down the old service and start up the new one, registering
298      * the new subdomain and cleaning up the old one.
299      *
300      * Properties are tied to the ID, which will not change.
301      *
302      * @param serviceID The ID of the service to be updated.
303      * @param subdomain New subdomain to assign to the service.
304      * @param description New description to assign to the service.
305      * @throws NotFoundException if service was not found.
306      */
updateMultiUserChatService(final long serviceID, @Nonnull final String subdomain, @Nullable final String description)307     public void updateMultiUserChatService(final long serviceID, @Nonnull final String subdomain, @Nullable final String description) throws NotFoundException {
308         final MultiUserChatServiceImpl muc = (MultiUserChatServiceImpl) getMultiUserChatService(serviceID);
309         if (muc == null) {
310             // A NotFoundException is thrown if the specified service was not found.
311             Log.info("Unable to find service to update for {}", serviceID);
312             throw new NotFoundException();
313         }
314         Log.info("Updating MUC service '{}'", subdomain);
315 
316         final String oldSubdomain = muc.getServiceName();
317         if (!mucServices.containsKey(oldSubdomain)) {
318             // This should never occur, but just in case...
319             throw new NotFoundException();
320         }
321         if (oldSubdomain.equals(subdomain)) {
322             // Alright, all we're changing is the description. This is easy.
323             updateService(serviceID, subdomain, description);
324             // Update the existing service's description.
325             muc.setDescription(description);
326             // Broadcast change to other cluster nodes (OF-2164)
327             CacheFactory.doSynchronousClusterTask(new ServiceUpdatedEvent(subdomain), false);
328         }
329         else {
330             // Changing the subdomain, here's where it   gets complex.
331 
332             // Unregister existing muc service
333             unregisterMultiUserChatService(subdomain, false);
334 
335             // Update the information stored about the MUC service
336             updateService(serviceID, subdomain, description);
337 
338             // Create new MUC service with new settings
339             final MultiUserChatService replacement = new MultiUserChatServiceImpl(subdomain, description, muc.isHidden());
340 
341             // Register to new service
342             registerMultiUserChatService(replacement, false);
343 
344             // Broadcast change(s) to other cluster nodes (OF-2164)
345             CacheFactory.doSynchronousClusterTask(new ServiceAddedEvent(subdomain, description, muc.isHidden()), false);
346             CacheFactory.doSynchronousClusterTask(new ServiceRemovedEvent(oldSubdomain), false);
347         }
348     }
349 
350     /**
351      * Updates the configuration of a MUC service.
352      *
353      * This is more involved than it may seem.
354      *
355      * If the subdomain is changed, we need to shut down the old service and start up the new one, registering the new
356      * subdomain and cleaning up the old one.
357      *
358      * Properties are tied to the ID, which will not change.
359      *
360      * @param currentSubdomain The current subdomain assigned to the service.
361      * @param newSubdomain New subdomain to assign to the service.
362      * @param description New description to assign to the service.
363      * @throws NotFoundException if service was not found.
364      */
updateMultiUserChatService(@onnull final String currentSubdomain, @Nonnull final String newSubdomain, @Nullable final String description)365     public void updateMultiUserChatService(@Nonnull final String currentSubdomain, @Nonnull final String newSubdomain, @Nullable final String description) throws NotFoundException {
366         final Long serviceID = getMultiUserChatServiceID(currentSubdomain);
367         if (serviceID == null) {
368             Log.info("Unable to find service to update for {}", currentSubdomain);
369             throw new NotFoundException();
370         }
371         updateMultiUserChatService(serviceID, newSubdomain, description);
372     }
373 
374     /**
375      * Deletes a configured MultiUserChatService by subdomain, and shuts it down.
376      *
377      * @param subdomain The subdomain of the service to be deleted.
378      * @throws NotFoundException if the service was not found.
379      */
removeMultiUserChatService(@onnull final String subdomain)380     public void removeMultiUserChatService(@Nonnull final String subdomain) throws NotFoundException {
381         final Long serviceID = getMultiUserChatServiceID(subdomain);
382         if (serviceID == null) {
383             Log.info("Unable to find service to remove for {}", subdomain);
384             throw new NotFoundException();
385         }
386         removeMultiUserChatService(serviceID);
387     }
388 
389     /**
390      * Deletes a configured MultiUserChatService by ID, and shuts it down.
391      *
392      * @param serviceID The ID opf the service to be deleted.
393      * @throws NotFoundException if the service was not found.
394      */
removeMultiUserChatService(final long serviceID)395     public void removeMultiUserChatService(final long serviceID) throws NotFoundException {
396         final MultiUserChatServiceImpl muc = (MultiUserChatServiceImpl) getMultiUserChatService(serviceID);
397         if (muc == null) {
398             Log.info("Unable to find service to remove for service ID {}", serviceID);
399             throw new NotFoundException();
400         }
401         final String subdomain = muc.getServiceName();
402         Log.info("Removing MUC service '{}'", subdomain);
403         unregisterMultiUserChatService(subdomain);
404         deleteService(serviceID);
405     }
406 
407     /**
408      * Retrieves a MultiUserChatService instance specified by it's service ID.
409      *
410      * @param serviceID ID of the conference service you wish to query.
411      * @return The MultiUserChatService instance associated with the id, or null if none found.
412      */
413     @Nullable
getMultiUserChatService(final long serviceID)414     public MultiUserChatService getMultiUserChatService(final long serviceID) {
415         final String subdomain = getMultiUserChatSubdomain(serviceID);
416         if (subdomain == null) {
417             return null;
418         }
419         return mucServices.get(subdomain);
420     }
421 
422     /**
423      * Retrieves a MultiUserChatService instance specified by it's subdomain of the server's primary domain. In other
424      * words: if the service is <tt>conference.example.org</tt>, and the server is <tt>example.org</tt>, you would
425      * specify <tt>conference</tt> here.
426      *
427      * @param subdomain Subdomain of the conference service you wish to query.
428      * @return The MultiUserChatService instance associated with the subdomain, or null if none found.
429      */
430     @Nullable
getMultiUserChatService(@onnull final String subdomain)431     public MultiUserChatService getMultiUserChatService(@Nonnull final String subdomain) {
432         return mucServices.get(subdomain);
433     }
434 
435     /**
436      * Retrieves a MultiUserChatService instance specified by any JID that refers to it. In other words: the argument
437      * value can be a XMPP domain name for the service, a room JID, or even the JID of a occupant of the room. The
438      * implementation takes the domain part of the JID, strips off the server domain name from the end, leaving only the
439      * subdomain, and then calls the subdomain version of the call.
440      *
441      * @param jid JID that contains a reference to the conference service.
442      * @return The MultiUserChatService instance associated with the JID, or null if none found.
443      */
444     @Nullable
getMultiUserChatService(@onnull final JID jid)445     public MultiUserChatService getMultiUserChatService(@Nonnull final JID jid) {
446         final String subdomain = jid.getDomain().replace("."+ XMPPServer.getInstance().getServerInfo().getXMPPDomain(), "");
447         return getMultiUserChatService(subdomain);
448     }
449 
450     /**
451      * Retrieves all of the MultiUserChatServices managed and configured for this server, sorted by subdomain.
452      *
453      * @return A list of MultiUserChatServices configured for this server.
454      */
455     @Nonnull
getMultiUserChatServices()456     public List<MultiUserChatService> getMultiUserChatServices() {
457         final List<MultiUserChatService> services = new ArrayList<>(mucServices.values());
458         services.sort(new ServiceComparator());
459         return services;
460     }
461 
462     /**
463      * Retrieves the number of MultiUserChatServices that are configured for this server.
464      *
465      * @return The number of registered MultiUserChatServices.
466      */
getMultiUserChatServicesCount()467     public int getMultiUserChatServicesCount() {
468         return mucServices.size();
469     }
470 
471     /**
472      * Returns true if a MUC service is configured/exists for a given subdomain.
473      *
474      * @param subdomain Subdomain of service to check on.
475      * @return True or false if the subdomain is registered as a MUC service.
476      */
isServiceRegistered(@ullable final String subdomain)477     public boolean isServiceRegistered(@Nullable final String subdomain) {
478         if (subdomain == null) {
479             return false;
480         }
481         return mucServices.containsKey(subdomain);
482     }
483 
484     /**
485      * Retrieves the database ID of a MUC service by subdomain.
486      *
487      * @param subdomain Subdomain of service to get ID of.
488      * @return ID number of MUC service, or null if none found.
489      */
getMultiUserChatServiceID(@onnull final String subdomain)490     public Long getMultiUserChatServiceID(@Nonnull final String subdomain) {
491         return loadServiceID(subdomain);
492     }
493 
494     /**
495      * Retrieves the subdomain of a specified service ID.
496      *
497      * @param serviceID ID of service to get subdomain of.
498      * @return Subdomain of MUC service, or null if none found.
499      */
500     @Nullable
getMultiUserChatSubdomain(final long serviceID)501     public String getMultiUserChatSubdomain(final long serviceID) {
502         return loadServiceSubdomain(serviceID);
503     }
504 
505     /**
506      * Loads the list of configured services stored in the database.
507      *
508      * This call will add the services to memory on the local cluster node, but will not propagate them to other nodes
509      * in the cluster.
510      */
loadServices()511     private void loadServices() {
512         Log.debug("Loading all MUC services from the database.");
513         Connection con = null;
514         PreparedStatement pstmt = null;
515         ResultSet rs = null;
516         try {
517             con = DbConnectionManager.getConnection();
518             pstmt = con.prepareStatement(LOAD_SERVICES);
519             rs = pstmt.executeQuery();
520             while (rs.next()) {
521                 String subdomain = rs.getString(1);
522                 String description = rs.getString(2);
523                 Boolean isHidden = Boolean.valueOf(rs.getString(3));
524                 final MultiUserChatServiceImpl muc = new MultiUserChatServiceImpl(subdomain, description, isHidden);
525 
526                 Log.trace("... loaded '{}' MUC service from the database.", subdomain);
527                 mucServices.put(subdomain, muc);
528             }
529         }
530         catch (Exception e) {
531             Log.error("An unexpected exception occurred while trying to load all MUC services from the database.", e);
532         }
533         finally {
534             DbConnectionManager.closeConnection(rs, pstmt, con);
535         }
536     }
537 
538     /**
539      * Updates the in-memory representation of a previously loaded services from the database.
540      *
541      * This call will modify database-stored characteristics for a service previously loaded to memory on the local
542      * cluster node. An exception will be thrown if used for a service that's not in memory.
543      *
544      * Note that this method will not cause MUCServiceProperties to be reloaded. It only operates on fields like the
545      * service description.
546      *
547      * This method is primarily useful to cause a service to reload its state from the database after it was changed on
548      * another cluster node.
549      *
550      * @param subdomain the domain of the service to refresh
551      */
refreshService(String subdomain)552     public void refreshService(String subdomain) {
553         Log.debug("Refreshing MUC service {} from the database.", subdomain);
554         if (!mucServices.containsKey(subdomain)) {
555             throw new IllegalArgumentException("Cannot refresh a MUC service that is not loaded: " + subdomain);
556         }
557         Connection con = null;
558         PreparedStatement pstmt = null;
559         ResultSet rs = null;
560         try {
561             con = DbConnectionManager.getConnection();
562             pstmt = con.prepareStatement(LOAD_SERVICE);
563             pstmt.setString(1, subdomain);
564             rs = pstmt.executeQuery();
565             if (rs.next()) {
566                 String description = rs.getString(1);
567                 Boolean isHidden = Boolean.valueOf(rs.getString(2));
568                 ((MultiUserChatServiceImpl)mucServices.get(subdomain)).setDescription(description);
569                 ((MultiUserChatServiceImpl)mucServices.get(subdomain)).setHidden(isHidden);
570             }
571             else {
572                 throw new Exception("Unable to locate database row for subdomain " + subdomain);
573             }
574         }
575         catch (Exception e) {
576             Log.error("A database exception occurred while trying to refresh MUC service '{}' from the database.", subdomain, e);
577         }
578         finally {
579             DbConnectionManager.closeConnection(rs, pstmt, con);
580         }
581         Log.trace("Refreshed MUC service '{}'", subdomain);
582     }
583 
584     /**
585      * Gets a specific subdomain/service's ID number.
586      *
587      * @param subdomain Subdomain to retrieve ID for.
588      * @return ID number of service, or null if no such service was found
589      */
590     @Nullable
loadServiceID(@onnull final String subdomain)591     private Long loadServiceID(@Nonnull final String subdomain) {
592         Connection con = null;
593         PreparedStatement pstmt = null;
594         ResultSet rs = null;
595         Long id = null;
596         try {
597             con = DbConnectionManager.getConnection();
598             pstmt = con.prepareStatement(LOAD_SERVICE_ID);
599             pstmt.setString(1, subdomain);
600             rs = pstmt.executeQuery();
601             if (rs.next()) {
602                 id = rs.getLong(1);
603             }
604             else {
605                 throw new Exception("Unable to locate Service ID for subdomain "+subdomain);
606             }
607         }
608         catch (Exception e) {
609             Log.error("A database exception occurred while trying to load the ID for MUC service '{}' from the database.", subdomain, e);
610         }
611         finally {
612             DbConnectionManager.closeConnection(rs, pstmt, con);
613         }
614         Log.trace("Loaded service ID for MUC service '{}'", subdomain);
615         return id;
616     }
617 
618     /**
619      * Gets a specific subdomain by a service's ID number.
620      *
621      * @param serviceID ID to retrieve subdomain for.
622      * @return Subdomain of service, or null if no such service was found.
623      */
624     @Nullable
loadServiceSubdomain(final long serviceID)625     private String loadServiceSubdomain(final long serviceID) {
626         Connection con = null;
627         PreparedStatement pstmt = null;
628         ResultSet rs = null;
629         String subdomain = null;
630         try {
631             con = DbConnectionManager.getConnection();
632             pstmt = con.prepareStatement(LOAD_SUBDOMAIN);
633             pstmt.setLong(1, serviceID);
634             rs = pstmt.executeQuery();
635             if (rs.next()) {
636                 subdomain = rs.getString(1);
637             }
638         }
639         catch (Exception e) {
640             Log.error("A database exception occurred while trying to load the subdomain for MUC service with database ID {} from the database.", serviceID, e);
641         }
642         finally {
643             DbConnectionManager.closeConnection(rs, pstmt, con);
644         }
645         Log.trace("Loaded service name for service with ID {}", serviceID);
646         return subdomain;
647     }
648 
649     /**
650      * Inserts a new MUC service into the database.
651      *
652      * @param subdomain Subdomain of new service.
653      * @param description Description of MUC service. Can be null for default description.
654      * @param isHidden True if the service should be hidden from service listing.
655      */
insertService(@onnull final String subdomain, @Nullable final String description, final boolean isHidden)656     private void insertService(@Nonnull final String subdomain, @Nullable final String description, final boolean isHidden) {
657         Connection con = null;
658         PreparedStatement pstmt = null;
659         final long serviceID = SequenceManager.nextID(JiveConstants.MUC_SERVICE);
660         try {
661             con = DbConnectionManager.getConnection();
662             pstmt = con.prepareStatement(CREATE_SERVICE);
663             pstmt.setLong(1, serviceID);
664             pstmt.setString(2, subdomain);
665             if (description != null) {
666                 pstmt.setString(3, description);
667             }
668             else {
669                 pstmt.setNull(3, Types.VARCHAR);
670             }
671             pstmt.setInt(4, (isHidden ? 1 : 0));
672             pstmt.executeUpdate();
673             Log.debug("Inserted MUC service '{}' with database ID {}", subdomain, serviceID);
674         }
675         catch (SQLException e) {
676             Log.error("A database exception occurred while trying to insert service '{}' to the database.", subdomain, e);
677         }
678         finally {
679             DbConnectionManager.closeConnection(pstmt, con);
680         }
681     }
682 
683     /**
684      * Updates an existing service's subdomain and description in the database.
685      *
686      * @param serviceID ID of the service to update.
687      * @param subdomain Subdomain to set service to.
688      * @param description Description of MUC service. Can be null for default description.
689      */
updateService(final long serviceID, @Nonnull final String subdomain, @Nullable final String description)690     private void updateService(final long serviceID, @Nonnull final String subdomain, @Nullable final String description) {
691         Connection con = null;
692         PreparedStatement pstmt = null;
693         try {
694             con = DbConnectionManager.getConnection();
695             pstmt = con.prepareStatement(UPDATE_SERVICE);
696             pstmt.setString(1, subdomain);
697             if (description != null) {
698                 pstmt.setString(2, description);
699             }
700             else {
701                 pstmt.setNull(2, Types.VARCHAR);
702             }
703             pstmt.setLong(3, serviceID);
704             pstmt.executeUpdate();
705             Log.debug("Updated MUC service '{}' with database ID {}", subdomain, serviceID);
706         }
707         catch (SQLException e) {
708             Log.error("A database exception occurred while trying to update service with ID {} in the database.", serviceID, e);
709         }
710         finally {
711             DbConnectionManager.closeConnection(pstmt, con);
712         }
713     }
714 
715     /**
716      * Deletes a service based on service ID.
717      *
718      * @param serviceID ID of the service to delete.
719      */
deleteService(final long serviceID)720     private void deleteService(final long serviceID) {
721         Connection con = null;
722         PreparedStatement pstmt = null;
723         try {
724             con = DbConnectionManager.getConnection();
725             pstmt = con.prepareStatement(DELETE_SERVICE);
726             pstmt.setLong(1, serviceID);
727             pstmt.executeUpdate();
728             Log.debug("Deleted MUC service with database ID {}", serviceID);
729         }
730         catch (SQLException e) {
731             Log.error("A database exception occurred while trying to remove service with ID {} from the database.", serviceID, e);
732         }
733         finally {
734             DbConnectionManager.closeConnection(pstmt, con);
735         }
736     }
737 
738     /****************** Statistics code ************************/
addTotalRoomStats()739     private void addTotalRoomStats() {
740         // Register a statistic.
741         final Statistic statistic = new Statistic() {
742             @Override
743             public String getName() {
744                 return LocaleUtils.getLocalizedString("muc.stats.active_group_chats.name");
745             }
746 
747             @Override
748             public Type getStatType() {
749                 return Type.count;
750             }
751 
752             @Override
753             public String getDescription() {
754                 return LocaleUtils.getLocalizedString("muc.stats.active_group_chats.desc");
755             }
756 
757             @Override
758             public String getUnits() {
759                 return LocaleUtils.getLocalizedString("muc.stats.active_group_chats.units");
760             }
761 
762             @Override
763             public double sample() {
764                 double rooms = 0;
765                 for (MultiUserChatService service : getMultiUserChatServices()) {
766                     rooms += service.getNumberChatRooms();
767                 }
768                 return rooms;
769             }
770 
771             @Override
772             public boolean isPartialSample() {
773                 return false;
774             }
775         };
776         StatisticsManager.getInstance().addStatistic(roomsStatKey, statistic);
777     }
778 
addTotalOccupantsStats()779     private void addTotalOccupantsStats() {
780         // Register a statistic.
781         final Statistic statistic = new Statistic() {
782             @Override
783             public String getName() {
784                 return LocaleUtils.getLocalizedString("muc.stats.occupants.name");
785             }
786 
787             @Override
788             public Type getStatType() {
789                 return Type.count;
790             }
791 
792             @Override
793             public String getDescription() {
794                 return LocaleUtils.getLocalizedString("muc.stats.occupants.description");
795             }
796 
797             @Override
798             public String getUnits() {
799                 return LocaleUtils.getLocalizedString("muc.stats.occupants.label");
800             }
801 
802             @Override
803             public double sample() {
804                 double occupants = 0;
805                 for (MultiUserChatService service : getMultiUserChatServices()) {
806                     occupants += service.getNumberRoomOccupants();
807                 }
808                 return occupants;
809             }
810 
811             @Override
812             public boolean isPartialSample() {
813                 return false;
814             }
815         };
816         StatisticsManager.getInstance().addStatistic(occupantsStatKey, statistic);
817     }
818 
addTotalConnectedUsers()819     private void addTotalConnectedUsers() {
820         // Register a statistic.
821         final Statistic statistic = new Statistic() {
822             @Override
823             public String getName() {
824                 return LocaleUtils.getLocalizedString("muc.stats.users.name");
825             }
826 
827             @Override
828             public Type getStatType() {
829                 return Type.count;
830             }
831 
832             @Override
833             public String getDescription() {
834                 return LocaleUtils.getLocalizedString("muc.stats.users.description");
835             }
836 
837             @Override
838             public String getUnits() {
839                 return LocaleUtils.getLocalizedString("muc.stats.users.label");
840             }
841 
842             @Override
843             public double sample() {
844                 double users = 0;
845                 for (MultiUserChatService service : getMultiUserChatServices()) {
846                     users += service.getNumberConnectedUsers();
847                 }
848                 return users;
849             }
850 
851             @Override
852             public boolean isPartialSample() {
853                 return false;
854             }
855         };
856         StatisticsManager.getInstance().addStatistic(usersStatKey, statistic);
857     }
858 
addNumberIncomingMessages()859     private void addNumberIncomingMessages() {
860         // Register a statistic.
861         final Statistic statistic = new Statistic() {
862             @Override
863             public String getName() {
864                 return LocaleUtils.getLocalizedString("muc.stats.incoming.name");
865             }
866 
867             @Override
868             public Type getStatType() {
869                 return Type.rate;
870             }
871 
872             @Override
873             public String getDescription() {
874                 return LocaleUtils.getLocalizedString("muc.stats.incoming.description");
875             }
876 
877             @Override
878             public String getUnits() {
879                 return LocaleUtils.getLocalizedString("muc.stats.incoming.label");
880             }
881 
882             @Override
883             public double sample() {
884                 double msgcnt = 0;
885                 for (MultiUserChatService service : getMultiUserChatServices()) {
886                     msgcnt += service.getIncomingMessageCount(true);
887                 }
888                 return msgcnt;
889             }
890 
891             @Override
892             public boolean isPartialSample() {
893                 // Get this value from the other cluster nodes
894                 return true;
895             }
896         };
897         StatisticsManager.getInstance().addMultiStatistic(incomingStatKey, trafficStatGroup, statistic);
898     }
899 
addNumberOutgoingMessages()900     private void addNumberOutgoingMessages() {
901         // Register a statistic.
902         final Statistic statistic = new Statistic() {
903             @Override
904             public String getName() {
905                 return LocaleUtils.getLocalizedString("muc.stats.outgoing.name");
906             }
907 
908             @Override
909             public Type getStatType() {
910                 return Type.rate;
911             }
912 
913             @Override
914             public String getDescription() {
915                 return LocaleUtils.getLocalizedString("muc.stats.outgoing.description");
916             }
917 
918             @Override
919             public String getUnits() {
920                 return LocaleUtils.getLocalizedString("muc.stats.outgoing.label");
921             }
922 
923             @Override
924             public double sample() {
925                 double msgcnt = 0;
926                 for (MultiUserChatService service : getMultiUserChatServices()) {
927                     msgcnt += service.getOutgoingMessageCount(true);
928                 }
929                 return msgcnt;
930             }
931 
932             @Override
933             public boolean isPartialSample() {
934                 // Each cluster node knows the total across the cluster
935                 return false;
936             }
937         };
938         StatisticsManager.getInstance().addMultiStatistic(outgoingStatKey, trafficStatGroup, statistic);
939     }
940 
941     @Override
propertySet(String service, String property, Map<String, Object> params)942     public void propertySet(String service, String property, Map<String, Object> params) {
943         // Let everyone know we've had an update.
944         CacheFactory.doSynchronousClusterTask(new ServiceUpdatedEvent(service), false);
945     }
946 
947     @Override
propertyDeleted(String service, String property, Map<String, Object> params)948     public void propertyDeleted(String service, String property, Map<String, Object> params) {
949         // Let everyone know we've had an update.
950         CacheFactory.doSynchronousClusterTask(new ServiceUpdatedEvent(service), false);
951     }
952 
953     @Override
userCreated(User user, Map<String, Object> params)954     public void userCreated(User user, Map<String, Object> params) {
955         // Do nothing
956     }
957 
958     @Override
userDeleting(User user, Map<String, Object> params)959     public void userDeleting(User user, Map<String, Object> params) {
960         // Delete any affiliation of the user to any room of any MUC service
961         MUCPersistenceManager
962                 .removeAffiliationFromDB(XMPPServer.getInstance().createJID(user.getUsername(), null, true));
963         // TODO Delete any user information from the rooms loaded into memory (OF-2166)
964     }
965 
966     @Override
userModified(User user, Map<String, Object> params)967     public void userModified(User user, Map<String, Object> params) {
968         // Do nothing
969     }
970 
971     private static class ServiceComparator implements Comparator<MultiUserChatService> {
972         @Override
compare(MultiUserChatService o1, MultiUserChatService o2)973         public int compare(MultiUserChatService o1, MultiUserChatService o2) {
974             return o1.getServiceName().compareTo(o2.getServiceName());
975         }
976     }
977 
978     /**
979      * Verifies that caches and supporting structures around rooms and occupants are in a consistent state.
980      *
981      * Note that this operation can be costly in terms of resource usage. Use with caution in large / busy systems.
982      *
983      * The returned multi-map can contain up to four keys: info, fail, pass, data. All entry values are a human readable
984      * description of a checked characteristic. When the state is consistent, no 'fail' entries will be returned.
985      *
986      * @return A consistency state report.
987      */
clusteringStateConsistencyReportForMucRoomsAndOccupant()988     public List<Multimap<String, String>> clusteringStateConsistencyReportForMucRoomsAndOccupant() {
989         return XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatServices().stream()
990             .map(mucService -> ConsistencyChecks.generateReportForMucRooms(
991                 mucService.getLocalMUCRoomManager().getROOM_CACHE(),
992                 mucService.getLocalMUCRoomManager().getLocalRooms(),
993                 mucService.getOccupantManager().getOccupantsByNode(),
994                 mucService.getOccupantManager().getNodesByOccupant(),
995                 mucService.getServiceName()
996             )).collect(Collectors.toList());
997     }
998 }
999