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