1 /*
2  * Copyright (C) 2004-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 
17 package org.jivesoftware.openfire.muc.spi;
18 
19 import org.jivesoftware.database.DbConnectionManager;
20 import org.jivesoftware.openfire.XMPPServer;
21 import org.jivesoftware.openfire.group.GroupJID;
22 import org.jivesoftware.openfire.muc.*;
23 import org.jivesoftware.util.JiveGlobals;
24 import org.jivesoftware.util.StringUtils;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27 import org.xmpp.packet.JID;
28 
29 import java.math.BigInteger;
30 import java.sql.*;
31 import java.util.Date;
32 import java.util.*;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.stream.Collectors;
35 import java.util.stream.Stream;
36 
37 import static org.jivesoftware.openfire.muc.spi.FMUCMode.MasterMaster;
38 
39 /**
40  * A manager responsible for ensuring room persistence. There are different ways to make a room
41  * persistent. The first attempt will be to save the room in a relation database. If for some reason
42  * the room can't be saved in the database an alternative repository will be used to save the room
43  * such as XML files.<p>
44  *
45  * After the problem with the database has been solved, the information saved in the XML files will
46  * be moved to the database.
47  *
48  * @author Gaston Dombiak
49  */
50 public class MUCPersistenceManager {
51 
52     private static final Logger Log = LoggerFactory.getLogger(MUCPersistenceManager.class);
53 
54     // property name for optional number of days to limit persistent MUC history during reload (OF-764)
55     private static final String MUC_HISTORY_RELOAD_LIMIT = "xmpp.muc.history.reload.limit";
56 
57     private static final String GET_RESERVED_NAME =
58         "SELECT nickname FROM ofMucMember WHERE roomID=? AND jid=?";
59     private static final String LOAD_ROOM =
60         "SELECT roomID, creationDate, modificationDate, naturalName, description, lockedDate, " +
61         "emptyDate, canChangeSubject, maxUsers, publicRoom, moderated, membersOnly, canInvite, " +
62         "roomPassword, canDiscoverJID, logEnabled, subject, rolesToBroadcast, useReservedNick, " +
63         "canChangeNick, canRegister, allowpm, fmucEnabled, fmucOutboundNode, fmucOutboundMode, " +
64         "fmucInboundNodes " +
65         " FROM ofMucRoom WHERE serviceID=? AND name=?";
66     private static final String LOAD_AFFILIATIONS =
67         "SELECT jid, affiliation FROM ofMucAffiliation WHERE roomID=?";
68     private static final String LOAD_MEMBERS =
69         "SELECT jid, nickname FROM ofMucMember WHERE roomID=?";
70     private static final String LOAD_HISTORY =
71         "SELECT sender, nickname, logTime, subject, body, stanza FROM ofMucConversationLog " +
72         "WHERE logTime>? AND roomID=? AND (nickname IS NOT NULL OR subject IS NOT NULL) ORDER BY logTime";
73     private static final String RELOAD_ALL_ROOMS_WITH_RECENT_ACTIVITY =
74         "SELECT roomID, creationDate, modificationDate, name, naturalName, description, " +
75         "lockedDate, emptyDate, canChangeSubject, maxUsers, publicRoom, moderated, membersOnly, " +
76         "canInvite, roomPassword, canDiscoverJID, logEnabled, subject, rolesToBroadcast, " +
77         "useReservedNick, canChangeNick, canRegister, allowpm, fmucEnabled, fmucOutboundNode, " +
78         "fmucOutboundMode, fmucInboundNodes " +
79         "FROM ofMucRoom WHERE serviceID=? AND (emptyDate IS NULL or emptyDate > ?)";
80     private static final String LOAD_ALL_ROOMS =
81         "SELECT roomID, creationDate, modificationDate, name, naturalName, description, " +
82         "lockedDate, emptyDate, canChangeSubject, maxUsers, publicRoom, moderated, membersOnly, " +
83         "canInvite, roomPassword, canDiscoverJID, logEnabled, subject, rolesToBroadcast, " +
84         "useReservedNick, canChangeNick, canRegister, allowpm, fmucEnabled, fmucOutboundNode, " +
85         "fmucOutboundMode, fmucInboundNodes " +
86         "FROM ofMucRoom WHERE serviceID=?";
87     private static final String COUNT_ALL_ROOMS =
88         "SELECT count(*) FROM ofMucRoom WHERE serviceID=?";
89     private static final String LOAD_ALL_ROOM_NAMES =
90         "SELECT name FROM ofMucRoom WHERE serviceID=?";
91     private static final String LOAD_ALL_AFFILIATIONS =
92         "SELECT ofMucAffiliation.roomID AS roomID, ofMucAffiliation.jid AS jid, ofMucAffiliation.affiliation AS affiliation " +
93         "FROM ofMucAffiliation,ofMucRoom WHERE ofMucAffiliation.roomID = ofMucRoom.roomID AND ofMucRoom.serviceID=?";
94     private static final String LOAD_ALL_MEMBERS =
95         "SELECT ofMucMember.roomID AS roomID, ofMucMember.jid AS jid, ofMucMember.nickname AS nickname FROM ofMucMember,ofMucRoom " +
96         "WHERE ofMucMember.roomID = ofMucRoom.roomID AND ofMucRoom.serviceID=?";
97     private static final String LOAD_ALL_HISTORY =
98         "SELECT ofMucConversationLog.roomID AS roomID, ofMucConversationLog.sender AS sender, ofMucConversationLog.nickname AS nickname, " +
99         "ofMucConversationLog.logTime AS logTime, ofMucConversationLog.subject AS subject, ofMucConversationLog.body AS body, ofMucConversationLog.stanza AS stanza FROM " +
100         "ofMucConversationLog, ofMucRoom WHERE ofMucConversationLog.roomID = ofMucRoom.roomID AND " +
101         "ofMucRoom.serviceID=? AND ofMucConversationLog.logTime>? AND (ofMucConversationLog.nickname IS NOT NULL " +
102         "OR ofMucConversationLog.subject IS NOT NULL) ORDER BY ofMucConversationLog.logTime";
103     private static final String UPDATE_ROOM =
104         "UPDATE ofMucRoom SET modificationDate=?, naturalName=?, description=?, " +
105         "canChangeSubject=?, maxUsers=?, publicRoom=?, moderated=?, membersOnly=?, " +
106         "canInvite=?, roomPassword=?, canDiscoverJID=?, logEnabled=?, rolesToBroadcast=?, " +
107         "useReservedNick=?, canChangeNick=?, canRegister=?, allowpm=?, fmucEnabled=?, " +
108         "fmucOutboundNode=?, fmucOutboundMode=?, fmucInboundNodes=? " +
109         "WHERE roomID=?";
110     private static final String ADD_ROOM =
111         "INSERT INTO ofMucRoom (serviceID, roomID, creationDate, modificationDate, name, naturalName, " +
112         "description, lockedDate, emptyDate, canChangeSubject, maxUsers, publicRoom, moderated, " +
113         "membersOnly, canInvite, roomPassword, canDiscoverJID, logEnabled, subject, " +
114         "rolesToBroadcast, useReservedNick, canChangeNick, canRegister, allowpm, fmucEnabled, fmucOutboundNode, " +
115         "fmucOutboundMode, fmucInboundNodes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
116     private static final String UPDATE_SUBJECT =
117         "UPDATE ofMucRoom SET subject=? WHERE roomID=?";
118     private static final String UPDATE_LOCK =
119         "UPDATE ofMucRoom SET lockedDate=? WHERE roomID=?";
120     private static final String UPDATE_EMPTYDATE =
121         "UPDATE ofMucRoom SET emptyDate=? WHERE roomID=?";
122     private static final String DELETE_ROOM =
123         "DELETE FROM ofMucRoom WHERE roomID=?";
124     private static final String DELETE_AFFILIATIONS =
125         "DELETE FROM ofMucAffiliation WHERE roomID=?";
126     private static final String DELETE_MEMBERS =
127         "DELETE FROM ofMucMember WHERE roomID=?";
128     private static final String ADD_MEMBER =
129         "INSERT INTO ofMucMember (roomID,jid,nickname) VALUES (?,?,?)";
130     private static final String UPDATE_MEMBER =
131         "UPDATE ofMucMember SET nickname=? WHERE roomID=? AND jid=?";
132     private static final String DELETE_MEMBER =
133         "DELETE FROM ofMucMember WHERE roomID=? AND jid=?";
134     private static final String ADD_AFFILIATION =
135         "INSERT INTO ofMucAffiliation (roomID,jid,affiliation) VALUES (?,?,?)";
136     private static final String UPDATE_AFFILIATION =
137         "UPDATE ofMucAffiliation SET affiliation=? WHERE roomID=? AND jid=?";
138     private static final String DELETE_AFFILIATION =
139         "DELETE FROM ofMucAffiliation WHERE roomID=? AND jid=?";
140     private static final String DELETE_USER_MEMBER =
141         "DELETE FROM ofMucMember WHERE jid=?";
142     private static final String DELETE_USER_MUCAFFILIATION =
143         "DELETE FROM ofMucAffiliation WHERE jid=?";
144     private static final String ADD_CONVERSATION_LOG =
145         "INSERT INTO ofMucConversationLog (roomID,messageID,sender,nickname,logTime,subject,body,stanza) VALUES (?,?,?,?,?,?,?,?)";
146 
147     /* Map of subdomains to their associated properties */
148     private static ConcurrentHashMap<String,MUCServiceProperties> propertyMaps = new ConcurrentHashMap<>();
149 
150     /**
151      * Returns the reserved room nickname for the bare JID in a given room or null if none.
152      *
153      * @param room the room where the user would like to obtain his reserved nickname.
154      * @param bareJID The bare jid of the user of which you'd like to obtain his reserved nickname.
155      * @return the reserved room nickname for the bare JID or null if none.
156      */
getReservedNickname(MUCRoom room, String bareJID)157     public static String getReservedNickname(MUCRoom room, String bareJID) {
158         Connection con = null;
159         PreparedStatement pstmt = null;
160         ResultSet rs = null;
161         String answer = null;
162         try {
163             con = DbConnectionManager.getConnection();
164             pstmt = con.prepareStatement(GET_RESERVED_NAME);
165             pstmt.setLong(1, room.getID());
166             pstmt.setString(2, bareJID);
167             rs = pstmt.executeQuery();
168             if (rs.next()) {
169                 answer = rs.getString("nickname");
170             }
171         }
172         catch (SQLException sqle) {
173             Log.error(sqle.getMessage(), sqle);
174         }
175         finally {
176             DbConnectionManager.closeConnection(rs, pstmt, con);
177         }
178         return answer;
179     }
180 
181     /**
182      * Counts all rooms of a chat service.
183      *
184      * Note that this method will count only rooms that are persisted in the database, and can exclude in-memory rooms
185      * that are not persisted.
186      *
187      * @param service the chat service for which to return a room count.
188      * @return A room number count
189      */
countRooms(MultiUserChatService service)190     public static int countRooms(MultiUserChatService service) {
191         Log.debug("Counting rooms for service '{}' in the database.", service.getServiceName());
192         Connection con = null;
193         PreparedStatement pstmt = null;
194         ResultSet rs = null;
195         try {
196             Long serviceID = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatServiceID(service.getServiceName());
197             con = DbConnectionManager.getConnection();
198             pstmt = con.prepareStatement(COUNT_ALL_ROOMS);
199             pstmt.setLong(1, serviceID);
200             rs = pstmt.executeQuery();
201             if (!rs.next()) {
202                 throw new IllegalArgumentException("Service " + service.getServiceName() + " was not found in the database.");
203             }
204             return rs.getInt(1);
205         } catch (SQLException sqle) {
206             Log.error("An exception occurred while trying to count all persisted rooms of service '{}'", service.getServiceName(), sqle);
207             return -1;
208         }
209         finally {
210             DbConnectionManager.closeConnection(rs, pstmt, con);
211         }
212     }
213 
214     /**
215      * Loads the room configuration from the database if the room was persistent.
216      *
217      * @param room the room to load from the database if persistent
218      */
loadFromDB(MUCRoom room)219     public static void loadFromDB(MUCRoom room) {
220         Log.debug("Attempting to load room '{}' from the database.", room.getName());
221         Connection con = null;
222         PreparedStatement pstmt = null;
223         ResultSet rs = null;
224         try {
225             Long serviceID = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatServiceID(room.getMUCService().getServiceName());
226             con = DbConnectionManager.getConnection();
227             pstmt = con.prepareStatement(LOAD_ROOM);
228             pstmt.setLong(1, serviceID);
229             pstmt.setString(2, room.getName());
230             rs = pstmt.executeQuery();
231             if (!rs.next()) {
232                 throw new IllegalArgumentException("Room " + room.getName() + " was not found in the database.");
233             }
234             room.setID(rs.getLong("roomID"));
235             room.setCreationDate(new Date(Long.parseLong(rs.getString("creationDate").trim())));
236             room.setModificationDate(new Date(Long.parseLong(rs.getString("modificationDate").trim())));
237             room.setNaturalLanguageName(rs.getString("naturalName"));
238             room.setDescription(rs.getString("description"));
239             room.setLockedDate(new Date(Long.parseLong(rs.getString("lockedDate").trim())));
240             if (rs.getString("emptyDate") != null) {
241                 room.setEmptyDate(new Date(Long.parseLong(rs.getString("emptyDate").trim())));
242             }
243             else {
244                 room.setEmptyDate(null);
245             }
246             room.setCanOccupantsChangeSubject(rs.getInt("canChangeSubject") == 1);
247             room.setMaxUsers(rs.getInt("maxUsers"));
248             room.setPublicRoom(rs.getInt("publicRoom") == 1);
249             room.setModerated(rs.getInt("moderated") == 1);
250             room.setMembersOnly(rs.getInt("membersOnly") == 1);
251             room.setCanOccupantsInvite(rs.getInt("canInvite") == 1);
252             room.setPassword(rs.getString("roomPassword"));
253             room.setCanAnyoneDiscoverJID(rs.getInt("canDiscoverJID") == 1);
254             room.setLogEnabled(rs.getInt("logEnabled") == 1);
255             room.setSubject(rs.getString("subject"));
256             List<MUCRole.Role> rolesToBroadcast = new ArrayList<>();
257             String roles = StringUtils.zeroPadString(Integer.toBinaryString(rs.getInt("rolesToBroadcast")), 3);
258             if (roles.charAt(0) == '1') {
259                 rolesToBroadcast.add(MUCRole.Role.moderator);
260             }
261             if (roles.charAt(1) == '1') {
262                 rolesToBroadcast.add(MUCRole.Role.participant);
263             }
264             if (roles.charAt(2) == '1') {
265                 rolesToBroadcast.add(MUCRole.Role.visitor);
266             }
267             room.setRolesToBroadcastPresence(rolesToBroadcast);
268             room.setLoginRestrictedToNickname(rs.getInt("useReservedNick") == 1);
269             room.setChangeNickname(rs.getInt("canChangeNick") == 1);
270             room.setRegistrationEnabled(rs.getInt("canRegister") == 1);
271             switch (rs.getInt("allowpm")) // null returns 0.
272             {
273                 default:
274                 case 0: room.setCanSendPrivateMessage( "anyone"       ); break;
275                 case 1: room.setCanSendPrivateMessage( "participants" ); break;
276                 case 2: room.setCanSendPrivateMessage( "moderators"   ); break;
277                 case 3: room.setCanSendPrivateMessage( "none"         ); break;
278             }
279             room.setFmucEnabled(rs.getInt("fmucEnabled") == 1);
280 
281             if ( rs.getString("fmucOutboundNode") != null ) {
282                 final JID fmucOutboundNode = new JID(rs.getString("fmucOutboundNode"));
283                 final FMUCMode fmucOutboundJoinMode;
284                 switch (rs.getInt("fmucOutboundMode")) // null returns 0.
285                 {
286                     default:
287                     case 0: fmucOutboundJoinMode = MasterMaster; break;
288                     case 1: fmucOutboundJoinMode = FMUCMode.MasterSlave; break;
289                 }
290                 room.setFmucOutboundNode( fmucOutboundNode );
291                 room.setFmucOutboundMode( fmucOutboundJoinMode );
292             } else {
293                 room.setFmucOutboundNode( null );
294                 room.setFmucOutboundMode( null );
295             }
296             if ( rs.getString("fmucInboundNodes") != null ) {
297                 final Set<JID> fmucInboundNodes = Stream.of(rs.getString("fmucInboundNodes").split("\n"))
298                                                         .map(String::trim)
299                                                         .map(JID::new)
300                                                         .collect(Collectors.toSet());
301                 // A list, which is an 'allow only on list' configuration. Note that the list can be empty (effectively: disallow all).
302                 room.setFmucInboundNodes(fmucInboundNodes);
303             } else {
304                 // Null: this is an 'allow all' configuration.
305                 room.setFmucInboundNodes(null);
306             }
307 
308             room.setPersistent(true);
309             DbConnectionManager.fastcloseStmt(rs, pstmt);
310 
311             // Recreate the history only for the rooms that have the conversation logging
312             // enabled
313             if (room.isLogEnabled()) {
314                 pstmt = con.prepareStatement(LOAD_HISTORY);
315                 // Reload the history, using "muc.history.reload.limit" (days); defaults to 2
316                 int reloadLimitDays = JiveGlobals.getIntProperty(MUC_HISTORY_RELOAD_LIMIT, 2);
317                 long from = System.currentTimeMillis() - (BigInteger.valueOf(86400000).multiply(BigInteger.valueOf(reloadLimitDays))).longValue();
318                 pstmt.setString(1, StringUtils.dateToMillis(new Date(from)));
319                 pstmt.setLong(2, room.getID());
320                 rs = pstmt.executeQuery();
321 
322                 while (rs.next()) {
323                     String senderJID = rs.getString("sender");
324                     String nickname = rs.getString("nickname");
325                     Date sentDate = new Date(Long.parseLong(rs.getString("logTime").trim()));
326                     String subject = rs.getString("subject");
327                     String body = rs.getString("body");
328                     String stanza = rs.getString("stanza");
329                     room.getRoomHistory().addOldMessage(senderJID, nickname, sentDate, subject, body, stanza);
330                 }
331             }
332             DbConnectionManager.fastcloseStmt(rs, pstmt);
333 
334             // If the room does not include the last subject in the history then recreate one if
335             // possible
336             if (!room.getRoomHistory().hasChangedSubject() && room.getSubject() != null &&
337                     room.getSubject().length() > 0) {
338                 room.getRoomHistory().addOldMessage(room.getRole().getRoleAddress().toString(),
339                         null, room.getModificationDate(), room.getSubject(), null, null);
340             }
341 
342             pstmt = con.prepareStatement(LOAD_AFFILIATIONS);
343             pstmt.setLong(1, room.getID());
344             rs = pstmt.executeQuery();
345             while (rs.next()) {
346                 // might be a group JID
347                 JID affiliationJID = GroupJID.fromString(rs.getString("jid"));
348                 MUCRole.Affiliation affiliation = MUCRole.Affiliation.valueOf(rs.getInt("affiliation"));
349                 try {
350                     switch (affiliation) {
351                         case owner:
352                             room.addOwner(affiliationJID, room.getRole());
353                             break;
354                         case admin:
355                             room.addAdmin(affiliationJID, room.getRole());
356                             break;
357                         case outcast:
358                             room.addOutcast(affiliationJID, null, room.getRole());
359                             break;
360                         default:
361                             Log.error("Unkown affiliation value " + affiliation + " for user "
362                                     + affiliationJID.toBareJID() + " in persistent room " + room.getID());
363                     }
364                 }
365                 catch (Exception e) {
366                     Log.error(e.getMessage(), e);
367                 }
368             }
369             DbConnectionManager.fastcloseStmt(rs, pstmt);
370 
371             pstmt = con.prepareStatement(LOAD_MEMBERS);
372             pstmt.setLong(1, room.getID());
373             rs = pstmt.executeQuery();
374             while (rs.next()) {
375                 try {
376                     room.addMember(new JID(rs.getString("jid")), rs.getString("nickname"), room.getRole());
377                 }
378                 catch (Exception e) {
379                     Log.error(e.getMessage(), e);
380                 }
381             }
382             // Set now that the room's configuration is updated in the database. Note: We need to
383             // set this now since otherwise the room's affiliations will be saved to the database
384             // "again" while adding them to the room!
385             room.setSavedToDB(true);
386             if (room.getEmptyDate() == null) {
387                 // The service process was killed somehow while the room was being used. Since
388                 // the room won't have occupants at this time we need to set the best date when
389                 // the last occupant left the room that we can
390                 room.setEmptyDate(new Date());
391             }
392         }
393         catch (SQLException sqle) {
394             Log.error(sqle.getMessage(), sqle);
395         }
396         finally {
397             DbConnectionManager.closeConnection(rs, pstmt, con);
398         }
399     }
400 
401     /**
402      * Save the room configuration to the DB.
403      *
404      * @param room The room to save its configuration.
405      */
saveToDB(MUCRoom room)406     public static void saveToDB(MUCRoom room) {
407         Log.debug("Attempting to save room '{}' to the database.", room.getName());
408         Connection con = null;
409         PreparedStatement pstmt = null;
410         try {
411             con = DbConnectionManager.getConnection();
412             if (room.wasSavedToDB()) {
413                 pstmt = con.prepareStatement(UPDATE_ROOM);
414                 pstmt.setString(1, StringUtils.dateToMillis(room.getModificationDate()));
415                 pstmt.setString(2, room.getNaturalLanguageName());
416                 pstmt.setString(3, room.getDescription());
417                 pstmt.setInt(4, (room.canOccupantsChangeSubject() ? 1 : 0));
418                 pstmt.setInt(5, room.getMaxUsers());
419                 pstmt.setInt(6, (room.isPublicRoom() ? 1 : 0));
420                 pstmt.setInt(7, (room.isModerated() ? 1 : 0));
421                 pstmt.setInt(8, (room.isMembersOnly() ? 1 : 0));
422                 pstmt.setInt(9, (room.canOccupantsInvite() ? 1 : 0));
423                 pstmt.setString(10, room.getPassword());
424                 pstmt.setInt(11, (room.canAnyoneDiscoverJID() ? 1 : 0));
425                 pstmt.setInt(12, (room.isLogEnabled() ? 1 : 0));
426                 pstmt.setInt(13, marshallRolesToBroadcast(room));
427                 pstmt.setInt(14, (room.isLoginRestrictedToNickname() ? 1 : 0));
428                 pstmt.setInt(15, (room.canChangeNickname() ? 1 : 0));
429                 pstmt.setInt(16, (room.isRegistrationEnabled() ? 1 : 0));
430                 switch (room.canSendPrivateMessage())
431                 {
432                     default:
433                     case "anyone":       pstmt.setInt(17, 0); break;
434                     case "participants": pstmt.setInt(17, 1); break;
435                     case "moderators":   pstmt.setInt(17, 2); break;
436                     case "none":         pstmt.setInt(17, 3); break;
437                 }
438                 pstmt.setInt(18, (room.isFmucEnabled() ? 1 : 0 ));
439                 if ( room.getFmucOutboundNode() == null ) {
440                     pstmt.setNull(19, Types.VARCHAR);
441                 } else {
442                     pstmt.setString(19, room.getFmucOutboundNode().toString());
443                 }
444                 if ( room.getFmucOutboundMode() == null ) {
445                     pstmt.setNull(20, Types.INTEGER);
446                 } else {
447                     pstmt.setInt(20, room.getFmucOutboundMode().equals(MasterMaster) ? 0 : 1);
448                 }
449 
450                 // Store a newline-separated collection, which is an 'allow only on list' configuration. Note that the list can be empty (effectively: disallow all), or null: this is an 'allow all' configuration.
451                 if (room.getFmucInboundNodes() == null) {
452                     pstmt.setNull(21, Types.VARCHAR); // Null: allow all.
453                 } else {
454                     final String content = room.getFmucInboundNodes().stream().map(JID::toString).collect(Collectors.joining("\n")); // result potentially is an empty String, but will not be null.
455                     pstmt.setString(21, content);
456                 }
457                 pstmt.setLong(22, room.getID());
458                 pstmt.executeUpdate();
459             }
460             else {
461                 pstmt = con.prepareStatement(ADD_ROOM);
462                 pstmt.setLong(1, XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatServiceID(room.getMUCService().getServiceName()));
463                 pstmt.setLong(2, room.getID());
464                 pstmt.setString(3, StringUtils.dateToMillis(room.getCreationDate()));
465                 pstmt.setString(4, StringUtils.dateToMillis(room.getModificationDate()));
466                 pstmt.setString(5, room.getName());
467                 pstmt.setString(6, room.getNaturalLanguageName());
468                 pstmt.setString(7, room.getDescription());
469                 pstmt.setString(8, StringUtils.dateToMillis(room.getLockedDate()));
470                 Date emptyDate = room.getEmptyDate();
471                 if (emptyDate == null) {
472                     pstmt.setString(9, null);
473                 }
474                 else {
475                     pstmt.setString(9, StringUtils.dateToMillis(emptyDate));
476                 }
477                 pstmt.setInt(10, (room.canOccupantsChangeSubject() ? 1 : 0));
478                 pstmt.setInt(11, room.getMaxUsers());
479                 pstmt.setInt(12, (room.isPublicRoom() ? 1 : 0));
480                 pstmt.setInt(13, (room.isModerated() ? 1 : 0));
481                 pstmt.setInt(14, (room.isMembersOnly() ? 1 : 0));
482                 pstmt.setInt(15, (room.canOccupantsInvite() ? 1 : 0));
483                 pstmt.setString(16, room.getPassword());
484                 pstmt.setInt(17, (room.canAnyoneDiscoverJID() ? 1 : 0));
485                 pstmt.setInt(18, (room.isLogEnabled() ? 1 : 0));
486                 pstmt.setString(19, room.getSubject());
487                 pstmt.setInt(20, marshallRolesToBroadcast(room));
488                 pstmt.setInt(21, (room.isLoginRestrictedToNickname() ? 1 : 0));
489                 pstmt.setInt(22, (room.canChangeNickname() ? 1 : 0));
490                 pstmt.setInt(23, (room.isRegistrationEnabled() ? 1 : 0));
491                 switch (room.canSendPrivateMessage())
492                 {
493                     default:
494                     case "anyone":       pstmt.setInt(24, 0); break;
495                     case "participants": pstmt.setInt(24, 1); break;
496                     case "moderators":   pstmt.setInt(24, 2); break;
497                     case "none":         pstmt.setInt(24, 3); break;
498                 }
499                 pstmt.setInt(25, (room.isFmucEnabled() ? 1 : 0 ));
500                 if ( room.getFmucOutboundNode() == null ) {
501                     pstmt.setNull(26, Types.VARCHAR);
502                 } else {
503                     pstmt.setString(26, room.getFmucOutboundNode().toString());
504                 }
505                 if ( room.getFmucOutboundMode() == null ) {
506                     pstmt.setNull(27, Types.INTEGER);
507                 } else {
508                     pstmt.setInt(27, room.getFmucOutboundMode().equals(MasterMaster) ? 0 : 1);
509                 }
510 
511                 // Store a newline-separated collection, which is an 'allow only on list' configuration. Note that the list can be empty (effectively: disallow all), or null: this is an 'allow all' configuration.
512                 if (room.getFmucInboundNodes() == null) {
513                     pstmt.setNull(28, Types.VARCHAR); // Null: allow all.
514                 } else {
515                     final String content = room.getFmucInboundNodes().stream().map(JID::toString).collect(Collectors.joining("\n")); // result potentially is an empty String, but will not be null.
516                     pstmt.setString(28, content);
517                 }
518                 pstmt.executeUpdate();
519             }
520         }
521         catch (SQLException sqle) {
522             Log.error(sqle.getMessage(), sqle);
523         }
524         finally {
525             DbConnectionManager.closeConnection(pstmt, con);
526         }
527     }
528 
529     /**
530      * Removes the room configuration and its affiliates from the database.
531      *
532      * @param room the room to remove from the database.
533      */
deleteFromDB(MUCRoom room)534     public static void deleteFromDB(MUCRoom room) {
535         Log.debug("Attempting to delete room '{}' from the database.", room.getName());
536 
537         if (!room.isPersistent() || !room.wasSavedToDB()) {
538             return;
539         }
540         Connection con = null;
541         PreparedStatement pstmt = null;
542         boolean abortTransaction = false;
543         try {
544             con = DbConnectionManager.getTransactionConnection();
545             pstmt = con.prepareStatement(DELETE_AFFILIATIONS);
546             pstmt.setLong(1, room.getID());
547             pstmt.executeUpdate();
548             DbConnectionManager.fastcloseStmt(pstmt);
549 
550             pstmt = con.prepareStatement(DELETE_MEMBERS);
551             pstmt.setLong(1, room.getID());
552             pstmt.executeUpdate();
553             DbConnectionManager.fastcloseStmt(pstmt);
554 
555             pstmt = con.prepareStatement(DELETE_ROOM);
556             pstmt.setLong(1, room.getID());
557             pstmt.executeUpdate();
558 
559             // Update the room (in memory) to indicate the it's no longer in the database.
560             room.setSavedToDB(false);
561         }
562         catch (SQLException sqle) {
563             Log.error(sqle.getMessage(), sqle);
564             abortTransaction = true;
565         }
566         finally {
567             DbConnectionManager.closeStatement(pstmt);
568             DbConnectionManager.closeTransactionConnection(con, abortTransaction);
569         }
570     }
571 
572     /**
573      * Loads the name of all the rooms that are in the database.
574      *
575      * @param chatserver the chat server that will hold the loaded rooms.
576      * @return a collection with all room names.
577      */
loadRoomNamesFromDB(MultiUserChatService chatserver)578     public static Collection<String> loadRoomNamesFromDB(MultiUserChatService chatserver) {
579         Log.debug("Loading room names for chat service {}", chatserver.getServiceName());
580         Long serviceID = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatServiceID(chatserver.getServiceName());
581 
582         final Set<String> names = new HashSet<>();
583         try {
584             Connection connection = null;
585             PreparedStatement statement = null;
586             ResultSet resultSet = null;
587             try {
588                 connection = DbConnectionManager.getConnection();
589                 statement = connection.prepareStatement(LOAD_ALL_ROOM_NAMES);
590                 statement.setLong(1, serviceID);
591                 resultSet = statement.executeQuery();
592 
593                 while (resultSet.next()) {
594                     try {
595                         names.add(resultSet.getString("name"));
596                     } catch (SQLException e) {
597                         Log.error("A database exception prevented one particular MUC room name to be loaded from the database.", e);
598                     }
599                 }
600             } finally {
601                 DbConnectionManager.closeConnection(resultSet, statement, connection);
602             }
603         }
604         catch (SQLException sqle) {
605             Log.error("A database error prevented MUC room names to be loaded from the database.", sqle);
606             return Collections.emptyList();
607         }
608 
609         Log.debug( "Loaded {} room names for chat service {}", names.size(), chatserver.getServiceName() );
610         return names;
611     }
612 
613     /**
614      * Loads all the rooms that had occupants after a given date from the database. This query
615      * will be executed only when the service is starting up.
616      *
617      * @param chatserver the chat server that will hold the loaded rooms.
618      * @param cleanupDate rooms that hadn't been used after this date won't be loaded.
619      * @return a collection with all the persistent rooms.
620      */
loadRoomsFromDB(MultiUserChatService chatserver, Date cleanupDate)621     public static Collection<MUCRoom> loadRoomsFromDB(MultiUserChatService chatserver, Date cleanupDate) {
622         Log.debug( "Loading rooms for chat service {}", chatserver.getServiceName() );
623         Long serviceID = XMPPServer.getInstance().getMultiUserChatManager().getMultiUserChatServiceID(chatserver.getServiceName());
624 
625         final Map<Long, MUCRoom> rooms;
626         try {
627             rooms = loadRooms(serviceID, cleanupDate, chatserver);
628             loadHistory(serviceID, rooms);
629             loadAffiliations(serviceID, rooms);
630             loadMembers(serviceID, rooms);
631         }
632         catch (SQLException sqle) {
633             Log.error("A database error prevented MUC rooms to be loaded from the database.", sqle);
634             return Collections.emptyList();
635         }
636 
637         // Set now that the room's configuration is updated in the database. Note: We need to
638         // set this now since otherwise the room's affiliations will be saved to the database
639         // "again" while adding them to the room!
640         for (final MUCRoom room : rooms.values()) {
641             room.setSavedToDB(true);
642             if (room.getEmptyDate() == null) {
643                 // The service process was killed somehow while the room was being used. Since
644                 // the room won't have occupants at this time we need to set the best date when
645                 // the last occupant left the room that we can
646                 room.setEmptyDate(new Date());
647             }
648         }
649         Log.debug( "Loaded {} rooms for chat service {}", rooms.size(), chatserver.getServiceName() );
650         return rooms.values();
651     }
652 
loadRooms(Long serviceID, Date cleanupDate, MultiUserChatService chatserver)653     private static Map<Long, MUCRoom> loadRooms(Long serviceID, Date cleanupDate, MultiUserChatService chatserver) throws SQLException {
654         final Map<Long, MUCRoom> rooms = new HashMap<>();
655 
656         Connection connection = null;
657         PreparedStatement statement = null;
658         ResultSet resultSet = null;
659         try {
660             connection = DbConnectionManager.getConnection();
661             if (cleanupDate!=null)
662             {
663                 statement = connection.prepareStatement(RELOAD_ALL_ROOMS_WITH_RECENT_ACTIVITY);
664                 statement.setLong(1, serviceID);
665                 statement.setString(2, StringUtils.dateToMillis(cleanupDate));
666             }
667             else
668             {
669                 statement = connection.prepareStatement(LOAD_ALL_ROOMS);
670                 statement.setLong(1, serviceID);
671             }
672             resultSet = statement.executeQuery();
673 
674             while (resultSet.next()) {
675                 try {
676                     MUCRoom room = new MUCRoom(chatserver, resultSet.getString("name"));
677                     room.setID(resultSet.getLong("roomID"));
678                     room.setCreationDate(new Date(Long.parseLong(resultSet.getString("creationDate").trim())));
679                     room.setModificationDate(new Date(Long.parseLong(resultSet.getString("modificationDate").trim())));
680                     room.setNaturalLanguageName(resultSet.getString("naturalName"));
681                     room.setDescription(resultSet.getString("description"));
682                     room.setLockedDate(new Date(Long.parseLong(resultSet.getString("lockedDate").trim())));
683                     if (resultSet.getString("emptyDate") != null) {
684                         room.setEmptyDate(new Date(Long.parseLong(resultSet.getString("emptyDate").trim())));
685                     }
686                     else {
687                         room.setEmptyDate(null);
688                     }
689                     room.setCanOccupantsChangeSubject(resultSet.getInt("canChangeSubject") == 1);
690                     room.setMaxUsers(resultSet.getInt("maxUsers"));
691                     room.setPublicRoom(resultSet.getInt("publicRoom") == 1);
692                     room.setModerated(resultSet.getInt("moderated") == 1);
693                     room.setMembersOnly(resultSet.getInt("membersOnly") == 1);
694                     room.setCanOccupantsInvite(resultSet.getInt("canInvite") == 1);
695                     room.setPassword(resultSet.getString("roomPassword"));
696                     room.setCanAnyoneDiscoverJID(resultSet.getInt("canDiscoverJID") == 1);
697                     room.setLogEnabled(resultSet.getInt("logEnabled") == 1);
698                     room.setSubject(resultSet.getString("subject"));
699                     List<MUCRole.Role> rolesToBroadcast = new ArrayList<>();
700                     String roles = StringUtils.zeroPadString(Integer.toBinaryString(resultSet.getInt("rolesToBroadcast")), 3);
701                     if (roles.charAt(0) == '1') {
702                         rolesToBroadcast.add(MUCRole.Role.moderator);
703                     }
704                     if (roles.charAt(1) == '1') {
705                         rolesToBroadcast.add(MUCRole.Role.participant);
706                     }
707                     if (roles.charAt(2) == '1') {
708                         rolesToBroadcast.add(MUCRole.Role.visitor);
709                     }
710                     room.setRolesToBroadcastPresence(rolesToBroadcast);
711                     room.setLoginRestrictedToNickname(resultSet.getInt("useReservedNick") == 1);
712                     room.setChangeNickname(resultSet.getInt("canChangeNick") == 1);
713                     room.setRegistrationEnabled(resultSet.getInt("canRegister") == 1);
714                     switch (resultSet.getInt("allowpm")) // null returns 0.
715                     {
716                         default:
717                         case 0: room.setCanSendPrivateMessage( "anyone"       ); break;
718                         case 1: room.setCanSendPrivateMessage( "participants" ); break;
719                         case 2: room.setCanSendPrivateMessage( "moderators"   ); break;
720                         case 3: room.setCanSendPrivateMessage( "none"         ); break;
721                     }
722 
723                     room.setFmucEnabled(resultSet.getInt("fmucEnabled") == 1);
724                     if ( resultSet.getString("fmucOutboundNode") != null ) {
725                         final JID fmucOutboundNode = new JID(resultSet.getString("fmucOutboundNode"));
726                         final FMUCMode fmucOutboundJoinMode;
727                         switch (resultSet.getInt("fmucOutboundMode")) // null returns 0.
728                         {
729                             default:
730                             case 0: fmucOutboundJoinMode = MasterMaster; break;
731                             case 1: fmucOutboundJoinMode = FMUCMode.MasterSlave; break;
732                         }
733                         room.setFmucOutboundNode( fmucOutboundNode );
734                         room.setFmucOutboundMode( fmucOutboundJoinMode );
735                     } else {
736                         room.setFmucOutboundNode( null );
737                         room.setFmucOutboundMode( null );
738                     }
739                     if ( resultSet.getString("fmucInboundNodes") != null ) {
740                         final Set<JID> fmucInboundNodes = Stream.of(resultSet.getString("fmucInboundNodes").split("\n"))
741                             .map(String::trim)
742                             .map(JID::new)
743                             .collect(Collectors.toSet());
744                         // A list, which is an 'allow only on list' configuration. Note that the list can be empty (effectively: disallow all).
745                         room.setFmucInboundNodes(fmucInboundNodes);
746                     } else {
747                         // Null: this is an 'allow all' configuration.
748                         room.setFmucInboundNodes(null);
749                     }
750 
751                     room.setPersistent(true);
752                     rooms.put(room.getID(), room);
753                 } catch (SQLException e) {
754                     Log.error("A database exception prevented one particular MUC room to be loaded from the database.", e);
755                 }
756             }
757         } finally {
758             DbConnectionManager.closeConnection(resultSet, statement, connection);
759         }
760 
761         return rooms;
762     }
763 
loadHistory(Long serviceID, Map<Long, MUCRoom> rooms)764     private static void loadHistory(Long serviceID, Map<Long, MUCRoom> rooms) throws SQLException {
765         Connection connection = null;
766         PreparedStatement statement = null;
767         ResultSet resultSet = null;
768         try {
769             connection = DbConnectionManager.getConnection();
770             statement = connection.prepareStatement(LOAD_ALL_HISTORY);
771 
772             // Reload the history, using "muc.history.reload.limit" (days) if present
773             long from = 0;
774             String reloadLimit = JiveGlobals.getProperty(MUC_HISTORY_RELOAD_LIMIT);
775             if (reloadLimit != null) {
776                 // if the property is defined, but not numeric, default to 2 (days)
777                 int reloadLimitDays = JiveGlobals.getIntProperty(MUC_HISTORY_RELOAD_LIMIT, 2);
778                 Log.warn("MUC history reload limit set to " + reloadLimitDays + " days");
779                 from = System.currentTimeMillis() - (BigInteger.valueOf(86400000).multiply(BigInteger.valueOf(reloadLimitDays))).longValue();
780             }
781             statement.setLong(1, serviceID);
782             statement.setString(2, StringUtils.dateToMillis(new Date(from)));
783             resultSet = statement.executeQuery();
784 
785             while (resultSet.next()) {
786                 try {
787                     MUCRoom room = rooms.get(resultSet.getLong("roomID"));
788                     // Skip to the next position if the room does not exist or if history is disabled
789                     if (room == null || !room.isLogEnabled()) {
790                         continue;
791                     }
792                     String senderJID = resultSet.getString("sender");
793                     String nickname  = resultSet.getString("nickname");
794                     Date sentDate    = new Date(Long.parseLong(resultSet.getString("logTime").trim()));
795                     String subject   = resultSet.getString("subject");
796                     String body      = resultSet.getString("body");
797                     String stanza    = resultSet.getString("stanza");
798                     room.getRoomHistory().addOldMessage(senderJID, nickname, sentDate, subject, body, stanza);
799                 } catch (SQLException e) {
800                     Log.warn("A database exception prevented the history for one particular MUC room to be loaded from the database.", e);
801                 }
802             }
803         } finally {
804             DbConnectionManager.closeConnection(resultSet, statement, connection);
805         }
806 
807         // Add the last known room subject to the room history only for those rooms that still
808         // don't have in their histories the last room subject
809         for (MUCRoom loadedRoom : rooms.values())
810         {
811             if (!loadedRoom.getRoomHistory().hasChangedSubject()
812                 && loadedRoom.getSubject() != null
813                 && loadedRoom.getSubject().length() > 0)
814             {
815                 loadedRoom.getRoomHistory().addOldMessage(  loadedRoom.getRole().getRoleAddress().toString(),
816                                                             null,
817                                                             loadedRoom.getModificationDate(),
818                                                             loadedRoom.getSubject(),
819                                                             null,
820                                                             null);
821             }
822         }
823     }
824 
loadAffiliations(Long serviceID, Map<Long, MUCRoom> rooms)825     private static void loadAffiliations(Long serviceID, Map<Long, MUCRoom> rooms) throws SQLException {
826         Connection connection = null;
827         PreparedStatement statement = null;
828         ResultSet resultSet = null;
829         try {
830             connection = DbConnectionManager.getConnection();
831             statement = connection.prepareStatement(LOAD_ALL_AFFILIATIONS);
832             statement.setLong(1, serviceID);
833             resultSet = statement.executeQuery();
834 
835             while (resultSet.next()) {
836                 try {
837                     long roomID = resultSet.getLong("roomID");
838                     MUCRoom room = rooms.get(roomID);
839                     // Skip to the next position if the room does not exist
840                     if (room == null) {
841                         continue;
842                     }
843 
844                     final MUCRole.Affiliation affiliation = MUCRole.Affiliation.valueOf(resultSet.getInt("affiliation"));
845 
846                     final String jidValue = resultSet.getString("jid");
847                     final JID affiliationJID;
848                     try {
849                         // might be a group JID
850                         affiliationJID = GroupJID.fromString(jidValue);
851                     } catch (IllegalArgumentException ex) {
852                         Log.warn("An illegal JID ({}) was found in the database, "
853                                 + "while trying to load all affiliations for room "
854                                 + "{}. The JID is ignored."
855                                 , new Object[] { jidValue, roomID });
856                         continue;
857                     }
858 
859                     try {
860                         switch (affiliation) {
861                             case owner:
862                                 room.addOwner(affiliationJID, room.getRole());
863                                 break;
864                             case admin:
865                                 room.addAdmin(affiliationJID, room.getRole());
866                                 break;
867                             case outcast:
868                                 room.addOutcast(affiliationJID, null, room.getRole());
869                                 break;
870                             default:
871                                 Log.error("Unknown affiliation value " + affiliation + " for user " + affiliationJID + " in persistent room " + room.getID());
872                         }
873                     } catch (ForbiddenException | ConflictException | NotAllowedException e) {
874                         Log.warn("An exception prevented affiliations to be added to the room with id " + roomID, e);
875                     }
876                 } catch (SQLException e) {
877                     Log.error("A database exception prevented affiliations for one particular MUC room to be loaded from the database.", e);
878                 }
879             }
880 
881         } finally {
882             DbConnectionManager.closeConnection(resultSet, statement, connection);
883         }
884     }
885 
loadMembers(Long serviceID, Map<Long, MUCRoom> rooms)886     private static void loadMembers(Long serviceID, Map<Long, MUCRoom> rooms) throws SQLException {
887         Connection connection = null;
888         PreparedStatement statement = null;
889         ResultSet resultSet = null;
890         JID affiliationJID = null;
891         try {
892             connection = DbConnectionManager.getConnection();
893             statement = connection.prepareStatement(LOAD_ALL_MEMBERS);
894             statement.setLong(1, serviceID);
895             resultSet = statement.executeQuery();
896 
897             while (resultSet.next()) {
898                 try {
899                     MUCRoom room = rooms.get(resultSet.getLong("roomID"));
900                     // Skip to the next position if the room does not exist
901                     if (room == null) {
902                         continue;
903                     }
904                     try {
905                         // might be a group JID
906                         affiliationJID = GroupJID.fromString(resultSet.getString("jid"));
907                         room.addMember(affiliationJID, resultSet.getString("nickname"), room.getRole());
908                     } catch (ForbiddenException | ConflictException e) {
909                         Log.warn("Unable to add member to room.", e);
910                     }
911                 } catch (SQLException e) {
912                     Log.error("A database exception prevented members for one particular MUC room to be loaded from the database.", e);
913                 }
914             }
915         } finally {
916             DbConnectionManager.closeConnection(resultSet, statement, connection);
917         }
918     }
919 
920     /**
921      * Updates the room's subject in the database.
922      *
923      * @param room the room to update its subject in the database.
924      */
updateRoomSubject(MUCRoom room)925     public static void updateRoomSubject(MUCRoom room) {
926         if (!room.isPersistent() || !room.wasSavedToDB()) {
927             return;
928         }
929 
930         Connection con = null;
931         PreparedStatement pstmt = null;
932         try {
933             con = DbConnectionManager.getConnection();
934             pstmt = con.prepareStatement(UPDATE_SUBJECT);
935             pstmt.setString(1, room.getSubject());
936             pstmt.setLong(2, room.getID());
937             pstmt.executeUpdate();
938         }
939         catch (SQLException sqle) {
940             Log.error(sqle.getMessage(), sqle);
941         }
942         finally {
943             DbConnectionManager.closeConnection(pstmt, con);
944         }
945     }
946 
947     /**
948      * Updates the room's lock status in the database.
949      *
950      * @param room the room to update its lock status in the database.
951      */
updateRoomLock(MUCRoom room)952     public static void updateRoomLock(MUCRoom room) {
953         if (!room.isPersistent() || !room.wasSavedToDB()) {
954             return;
955         }
956 
957         Connection con = null;
958         PreparedStatement pstmt = null;
959         try {
960             con = DbConnectionManager.getConnection();
961             pstmt = con.prepareStatement(UPDATE_LOCK);
962             pstmt.setString(1, StringUtils.dateToMillis(room.getLockedDate()));
963             pstmt.setLong(2, room.getID());
964             pstmt.executeUpdate();
965         }
966         catch (SQLException sqle) {
967             Log.error(sqle.getMessage(), sqle);
968         }
969         finally {
970             DbConnectionManager.closeConnection(pstmt, con);
971         }
972     }
973 
974     /**
975      * Updates the room's lock status in the database.
976      *
977      * @param room the room to update its lock status in the database.
978      */
updateRoomEmptyDate(MUCRoom room)979     public static void updateRoomEmptyDate(MUCRoom room) {
980         if (!room.isPersistent() || !room.wasSavedToDB()) {
981             return;
982         }
983 
984         Connection con = null;
985         PreparedStatement pstmt = null;
986         try {
987             con = DbConnectionManager.getConnection();
988             pstmt = con.prepareStatement(UPDATE_EMPTYDATE);
989             Date emptyDate = room.getEmptyDate();
990             if (emptyDate == null) {
991                 pstmt.setString(1, null);
992             }
993             else {
994                 pstmt.setString(1, StringUtils.dateToMillis(emptyDate));
995             }
996             pstmt.setLong(2, room.getID());
997             pstmt.executeUpdate();
998         }
999         catch (SQLException sqle) {
1000             Log.error(sqle.getMessage(), sqle);
1001         }
1002         finally {
1003             DbConnectionManager.closeConnection(pstmt, con);
1004         }
1005     }
1006 
1007     /**
1008      * Update the DB with the new affiliation of the user in the room. The new information will be
1009      * saved only if the room is_persistent and has already been saved to the database previously.
1010      *
1011      * @param room The room where the affiliation of the user was updated.
1012      * @param jid The bareJID of the user to update this affiliation.
1013      * @param nickname The reserved nickname of the user in the room or null if none.
1014      * @param newAffiliation the new affiliation of the user in the room.
1015      * @param oldAffiliation the previous affiliation of the user in the room.
1016      */
saveAffiliationToDB(MUCRoom room, JID jid, String nickname, MUCRole.Affiliation newAffiliation, MUCRole.Affiliation oldAffiliation)1017     public static void saveAffiliationToDB(MUCRoom room, JID jid, String nickname,
1018                                            MUCRole.Affiliation newAffiliation, MUCRole.Affiliation oldAffiliation)
1019     {
1020         final String affiliationJid = jid.toBareJID();
1021         if (!room.isPersistent() || !room.wasSavedToDB()) {
1022             return;
1023         }
1024         if (MUCRole.Affiliation.none == oldAffiliation) {
1025             if (MUCRole.Affiliation.member == newAffiliation) {
1026                 // Add the user to the members table
1027                 Connection con = null;
1028                 PreparedStatement pstmt = null;
1029                 try {
1030                     con = DbConnectionManager.getConnection();
1031                     pstmt = con.prepareStatement(ADD_MEMBER);
1032                     pstmt.setLong(1, room.getID());
1033                     pstmt.setString(2, affiliationJid);
1034                     pstmt.setString(3, nickname);
1035                     pstmt.executeUpdate();
1036                 }
1037                 catch (SQLException sqle) {
1038                     Log.error(sqle.getMessage(), sqle);
1039                 }
1040                 finally {
1041                     DbConnectionManager.closeConnection(pstmt, con);
1042                 }
1043             }
1044             else {
1045                 // Add the user to the generic affiliations table
1046                 Connection con = null;
1047                 PreparedStatement pstmt = null;
1048                 try {
1049                     con = DbConnectionManager.getConnection();
1050                     pstmt = con.prepareStatement(ADD_AFFILIATION);
1051                     pstmt.setLong(1, room.getID());
1052                     pstmt.setString(2, affiliationJid);
1053                     pstmt.setInt(3, newAffiliation.getValue());
1054                     pstmt.executeUpdate();
1055                 }
1056                 catch (SQLException sqle) {
1057                     Log.error(sqle.getMessage(), sqle);
1058                 }
1059                 finally {
1060                     DbConnectionManager.closeConnection(pstmt, con);
1061                 }
1062             }
1063         }
1064         else {
1065             if (MUCRole.Affiliation.member == newAffiliation &&
1066                     MUCRole.Affiliation.member == oldAffiliation)
1067             {
1068                 // Update the member's data in the member table.
1069                 Connection con = null;
1070                 PreparedStatement pstmt = null;
1071                 try {
1072                     con = DbConnectionManager.getConnection();
1073                     pstmt = con.prepareStatement(UPDATE_MEMBER);
1074                     pstmt.setString(1, nickname);
1075                     pstmt.setLong(2, room.getID());
1076                     pstmt.setString(3, affiliationJid);
1077                     pstmt.executeUpdate();
1078                 }
1079                 catch (SQLException sqle) {
1080                     Log.error(sqle.getMessage(), sqle);
1081                 }
1082                 finally {
1083                     DbConnectionManager.closeConnection(pstmt, con);
1084                 }
1085             }
1086             else if (MUCRole.Affiliation.member == newAffiliation) {
1087                 Connection con = null;
1088                 PreparedStatement pstmt = null;
1089                 boolean abortTransaction = false;
1090                 try {
1091                     // Remove the user from the generic affiliations table
1092                     con = DbConnectionManager.getTransactionConnection();
1093                     pstmt = con.prepareStatement(DELETE_AFFILIATION);
1094                     pstmt.setLong(1, room.getID());
1095                     pstmt.setString(2, affiliationJid);
1096                     pstmt.executeUpdate();
1097                     DbConnectionManager.fastcloseStmt(pstmt);
1098 
1099                     // Add them as a member.
1100                     pstmt = con.prepareStatement(ADD_MEMBER);
1101                     pstmt.setLong(1, room.getID());
1102                     pstmt.setString(2, affiliationJid);
1103                     pstmt.setString(3, nickname);
1104                     pstmt.executeUpdate();
1105                 }
1106                 catch (SQLException sqle) {
1107                     Log.error(sqle.getMessage(), sqle);
1108                     abortTransaction = true;
1109                 }
1110                 finally {
1111                     DbConnectionManager.closeStatement(pstmt);
1112                     DbConnectionManager.closeTransactionConnection(con, abortTransaction);
1113                 }
1114             }
1115             else if (MUCRole.Affiliation.member == oldAffiliation) {
1116                 Connection con = null;
1117                 PreparedStatement pstmt = null;
1118                 boolean abortTransaction = false;
1119                 try {
1120                     con = DbConnectionManager.getTransactionConnection();
1121                     pstmt = con.prepareStatement(DELETE_MEMBER);
1122                     pstmt.setLong(1, room.getID());
1123                     pstmt.setString(2, affiliationJid);
1124                     pstmt.executeUpdate();
1125                     DbConnectionManager.fastcloseStmt(pstmt);
1126 
1127                     pstmt = con.prepareStatement(ADD_AFFILIATION);
1128                     pstmt.setLong(1, room.getID());
1129                     pstmt.setString(2, affiliationJid);
1130                     pstmt.setInt(3, newAffiliation.getValue());
1131                     pstmt.executeUpdate();
1132                 }
1133                 catch (SQLException sqle) {
1134                     Log.error(sqle.getMessage(), sqle);
1135                     abortTransaction = true;
1136                 }
1137                 finally {
1138                     DbConnectionManager.closeStatement(pstmt);
1139                     DbConnectionManager.closeTransactionConnection(con, abortTransaction);
1140                 }
1141             }
1142             else {
1143                 // Update the user in the generic affiliations table.
1144                 Connection con = null;
1145                 PreparedStatement pstmt = null;
1146                 try {
1147                     con = DbConnectionManager.getConnection();
1148                     pstmt = con.prepareStatement(UPDATE_AFFILIATION);
1149                     pstmt.setInt(1, newAffiliation.getValue());
1150                     pstmt.setLong(2, room.getID());
1151                     pstmt.setString(3, affiliationJid);
1152                     pstmt.executeUpdate();
1153                 }
1154                 catch (SQLException sqle) {
1155                     Log.error(sqle.getMessage(), sqle);
1156                 }
1157                 finally {
1158                     DbConnectionManager.closeConnection(pstmt, con);
1159                 }
1160             }
1161         }
1162     }
1163 
1164     /**
1165      * Removes the affiliation of the user from the DB if the room is persistent.
1166      *
1167      * @param room The room where the affiliation of the user was removed.
1168      * @param jid The bareJID of the user to remove his affiliation.
1169      * @param oldAffiliation the previous affiliation of the user in the room.
1170      */
removeAffiliationFromDB(MUCRoom room, JID jid, MUCRole.Affiliation oldAffiliation)1171     public static void removeAffiliationFromDB(MUCRoom room, JID jid,
1172                                                MUCRole.Affiliation oldAffiliation)
1173     {
1174         final String affiliationJID = jid.toBareJID();
1175         if (room.isPersistent() && room.wasSavedToDB()) {
1176             if (MUCRole.Affiliation.member == oldAffiliation) {
1177                 // Remove the user from the members table
1178                 Connection con = null;
1179                 PreparedStatement pstmt = null;
1180                 try {
1181                     con = DbConnectionManager.getConnection();
1182                     pstmt = con.prepareStatement(DELETE_MEMBER);
1183                     pstmt.setLong(1, room.getID());
1184                     pstmt.setString(2, affiliationJID);
1185                     pstmt.executeUpdate();
1186                 }
1187                 catch (SQLException sqle) {
1188                     Log.error(sqle.getMessage(), sqle);
1189                 }
1190                 finally {
1191                     DbConnectionManager.closeConnection(pstmt, con);
1192                 }
1193             }
1194             else {
1195                 // Remove the user from the generic affiliations table
1196                 Connection con = null;
1197                 PreparedStatement pstmt = null;
1198                 try {
1199                     con = DbConnectionManager.getConnection();
1200                     pstmt = con.prepareStatement(DELETE_AFFILIATION);
1201                     pstmt.setLong(1, room.getID());
1202                     pstmt.setString(2, affiliationJID);
1203                     pstmt.executeUpdate();
1204                 }
1205                 catch (SQLException sqle) {
1206                     Log.error(sqle.getMessage(), sqle);
1207                 }
1208                 finally {
1209                     DbConnectionManager.closeConnection(pstmt, con);
1210                 }
1211             }
1212         }
1213     }
1214 
1215     /**
1216      * Removes the affiliation of the user from the DB if ANY room that is persistent.
1217      *
1218      * @param affiliationJID The bareJID of the user to remove his affiliation from ALL persistent rooms.
1219      */
removeAffiliationFromDB(JID affiliationJID)1220     public static void removeAffiliationFromDB(JID affiliationJID)
1221     {
1222         Connection con = null;
1223         PreparedStatement pstmt = null;
1224         try {
1225             con = DbConnectionManager.getConnection();
1226             // Remove the user from the members table
1227             pstmt = con.prepareStatement(DELETE_USER_MEMBER);
1228             pstmt.setString(1, affiliationJID.toBareJID());
1229             pstmt.executeUpdate();
1230             DbConnectionManager.fastcloseStmt(pstmt);
1231 
1232             // Remove the user from the generic affiliations table
1233             pstmt = con.prepareStatement(DELETE_USER_MUCAFFILIATION);
1234             pstmt.setString(1, affiliationJID.toBareJID());
1235             pstmt.executeUpdate();
1236         }
1237         catch (SQLException sqle) {
1238             Log.error(sqle.getMessage(), sqle);
1239         }
1240         finally {
1241             DbConnectionManager.closeConnection(pstmt, con);
1242         }
1243     }
1244 
1245     /**
1246      * Saves the conversation log entry batch to the database.
1247      *
1248      * @param batch a list of ConversationLogEntry to save to the database.
1249      * @return true if the batch was saved successfully to the database.
1250      */
saveConversationLogBatch(List<ConversationLogEntry> batch)1251     public static boolean saveConversationLogBatch(List<ConversationLogEntry> batch) {
1252         Connection con = null;
1253         PreparedStatement pstmt = null;
1254 
1255         try {
1256             con = DbConnectionManager.getConnection();
1257             pstmt = con.prepareStatement(ADD_CONVERSATION_LOG);
1258             con.setAutoCommit(false);
1259 
1260             for(ConversationLogEntry entry : batch) {
1261                 pstmt.setLong(1, entry.getRoomID());
1262                 pstmt.setLong(2, entry.getMessageID());
1263                 pstmt.setString(3, entry.getSender().toString());
1264                 pstmt.setString(4, entry.getNickname());
1265                 pstmt.setString(5, StringUtils.dateToMillis(entry.getDate()));
1266                 pstmt.setString(6, entry.getSubject());
1267                 pstmt.setString(7, entry.getBody());
1268                 pstmt.setString(8, entry.getStanza());
1269                 pstmt.addBatch();
1270             }
1271 
1272             pstmt.executeBatch();
1273             con.commit();
1274             return true;
1275         }
1276         catch (SQLException sqle) {
1277             Log.error("Error saving conversation log batch", sqle);
1278             if (con != null) {
1279             	try {
1280 					con.rollback();
1281 				} catch (SQLException ignore) {}
1282             }
1283             return false;
1284         }
1285         finally {
1286             DbConnectionManager.closeConnection(pstmt, con);
1287         }
1288     }
1289 
1290     /**
1291      * Returns an integer based on the binary representation of the roles to broadcast.
1292      *
1293      * @param room the room to marshall its roles to broadcast.
1294      * @return an integer based on the binary representation of the roles to broadcast.
1295      */
marshallRolesToBroadcast(MUCRoom room)1296     private static int marshallRolesToBroadcast(MUCRoom room) {
1297         final String buffer =
1298             (room.canBroadcastPresence(MUCRole.Role.moderator) ? "1" : "0") +
1299             (room.canBroadcastPresence(MUCRole.Role.participant) ? "1" : "0") +
1300             (room.canBroadcastPresence(MUCRole.Role.visitor) ? "1" : "0");
1301         return Integer.parseInt(buffer, 2);
1302     }
1303 
1304     /**
1305      * Returns a Jive property.
1306      *
1307      * @param subdomain the subdomain of the service to retrieve a property from
1308      * @param name the name of the property to return.
1309      * @return the property value specified by name.
1310      */
getProperty(String subdomain, String name)1311     public static String getProperty(String subdomain, String name) {
1312         final MUCServiceProperties props = propertyMaps.computeIfAbsent( subdomain, MUCServiceProperties::new );
1313         return props.get(name);
1314     }
1315 
1316     /**
1317      * Returns a Jive property. If the specified property doesn't exist, the
1318      * {@code defaultValue} will be returned.
1319      *
1320      * @param subdomain the subdomain of the service to retrieve a property from
1321      * @param name the name of the property to return.
1322      * @param defaultValue value returned if the property doesn't exist.
1323      * @return the property value specified by name.
1324      */
getProperty(String subdomain, String name, String defaultValue)1325     public static String getProperty(String subdomain, String name, String defaultValue) {
1326         final String value = getProperty(subdomain, name);
1327         if (value != null) {
1328             return value;
1329         } else {
1330             return defaultValue;
1331         }
1332     }
1333 
1334     /**
1335      * Returns an integer value Jive property. If the specified property doesn't exist, the
1336      * {@code defaultValue} will be returned.
1337      *
1338      * @param subdomain the subdomain of the service to retrieve a property from
1339      * @param name the name of the property to return.
1340      * @param defaultValue value returned if the property doesn't exist or was not
1341      *      a number.
1342      * @return the property value specified by name or {@code defaultValue}.
1343      */
getIntProperty(String subdomain, String name, int defaultValue)1344     public static int getIntProperty(String subdomain, String name, int defaultValue) {
1345         String value = getProperty(subdomain, name);
1346         if (value != null) {
1347             try {
1348                 return Integer.parseInt(value);
1349             }
1350             catch (NumberFormatException nfe) {
1351                 // Ignore.
1352             }
1353         }
1354         return defaultValue;
1355     }
1356 
1357     /**
1358      * Returns a long value Jive property. If the specified property doesn't exist, the
1359      * {@code defaultValue} will be returned.
1360      *
1361      * @param subdomain the subdomain of the service to retrieve a property from
1362      * @param name the name of the property to return.
1363      * @param defaultValue value returned if the property doesn't exist or was not
1364      *      a number.
1365      * @return the property value specified by name or {@code defaultValue}.
1366      */
getLongProperty(String subdomain, String name, long defaultValue)1367     public static long getLongProperty(String subdomain, String name, long defaultValue) {
1368         String value = getProperty(subdomain, name);
1369         if (value != null) {
1370             try {
1371                 return Long.parseLong(value);
1372             }
1373             catch (NumberFormatException nfe) {
1374                 // Ignore.
1375             }
1376         }
1377         return defaultValue;
1378     }
1379 
1380     /**
1381      * Returns a boolean value Jive property.
1382      *
1383      * @param subdomain the subdomain of the service to retrieve a property from
1384      * @param name the name of the property to return.
1385      * @return true if the property value exists and is set to {@code "true"} (ignoring case).
1386      *      Otherwise {@code false} is returned.
1387      */
getBooleanProperty(String subdomain, String name)1388     public static boolean getBooleanProperty(String subdomain, String name) {
1389         return Boolean.valueOf(getProperty(subdomain, name));
1390     }
1391 
1392     /**
1393      * Returns a boolean value Jive property. If the property doesn't exist, the {@code defaultValue}
1394      * will be returned.
1395      *
1396      * If the specified property can't be found, or if the value is not a number, the
1397      * {@code defaultValue} will be returned.
1398      *
1399      * @param subdomain the subdomain of the service to retrieve a property from
1400      * @param name the name of the property to return.
1401      * @param defaultValue value returned if the property doesn't exist.
1402      * @return true if the property value exists and is set to {@code "true"} (ignoring case).
1403      *      Otherwise {@code false} is returned.
1404      */
getBooleanProperty(String subdomain, String name, boolean defaultValue)1405     public static boolean getBooleanProperty(String subdomain, String name, boolean defaultValue) {
1406         String value = getProperty(subdomain, name);
1407         if (value != null) {
1408             return Boolean.valueOf(value);
1409         }
1410         else {
1411             return defaultValue;
1412         }
1413     }
1414 
1415     /**
1416      * Return all immediate children property names of a parent Jive property as a list of strings,
1417      * or an empty list if there are no children. For example, given
1418      * the properties {@code X.Y.A}, {@code X.Y.B}, {@code X.Y.C} and {@code X.Y.C.D}, then
1419      * the immediate child properties of {@code X.Y} are {@code A}, {@code B}, and
1420      * {@code C} ({@code C.D} would not be returned using this method).<p>
1421      *
1422      * @param subdomain the subdomain of the service to retrieve a property from
1423      * @param parent the root "node" of the properties to retrieve
1424      * @return a List of all immediate children property names (Strings).
1425      */
getPropertyNames(String subdomain, String parent)1426     public static List<String> getPropertyNames(String subdomain, String parent) {
1427         final MUCServiceProperties props = propertyMaps.computeIfAbsent( subdomain, MUCServiceProperties::new );
1428         return new ArrayList<>(props.getChildrenNames(parent));
1429     }
1430 
1431     /**
1432      * Return all immediate children property values of a parent Jive property as a list of strings,
1433      * or an empty list if there are no children. For example, given
1434      * the properties {@code X.Y.A}, {@code X.Y.B}, {@code X.Y.C} and {@code X.Y.C.D}, then
1435      * the immediate child properties of {@code X.Y} are {@code X.Y.A}, {@code X.Y.B}, and
1436      * {@code X.Y.C} (the value of {@code X.Y.C.D} would not be returned using this method).<p>
1437      *
1438      * @param subdomain the subdomain of the service to retrieve a property from
1439      * @param parent the name of the parent property to return the children for.
1440      * @return all child property values for the given parent.
1441      */
getProperties(String subdomain, String parent)1442     public static List<String> getProperties(String subdomain, String parent) {
1443         final MUCServiceProperties props = propertyMaps.computeIfAbsent( subdomain, MUCServiceProperties::new );
1444         Collection<String> propertyNames = props.getChildrenNames(parent);
1445         List<String> values = new ArrayList<>();
1446         for (String propertyName : propertyNames) {
1447             String value = getProperty(subdomain, propertyName);
1448             if (value != null) {
1449                 values.add(value);
1450             }
1451         }
1452 
1453         return values;
1454     }
1455 
1456     /**
1457      * Returns all MUC service property names.
1458      *
1459      * @param subdomain the subdomain of the service to retrieve a property from
1460      * @return a List of all property names (Strings).
1461      */
getPropertyNames(String subdomain)1462     public static List<String> getPropertyNames(String subdomain) {
1463         final MUCServiceProperties props = propertyMaps.computeIfAbsent( subdomain, MUCServiceProperties::new );
1464         return new ArrayList<>(props.getPropertyNames());
1465     }
1466 
1467     /**
1468      * Sets a Jive property. If the property doesn't already exists, a new
1469      * one will be created.
1470      *
1471      * @param subdomain the subdomain of the service to set a property for
1472      * @param name the name of the property being set.
1473      * @param value the value of the property being set.
1474      */
setProperty(String subdomain, String name, String value)1475     public static void setProperty(String subdomain, String name, String value) {
1476         MUCServiceProperties properties = propertyMaps.get(subdomain);
1477         if (properties == null) {
1478             properties = new MUCServiceProperties(subdomain);
1479         }
1480         properties.put(name, value);
1481         propertyMaps.put(subdomain, properties);
1482     }
1483 
setLocalProperty(String subdomain, String name, String value)1484     public static void setLocalProperty(String subdomain, String name, String value) {
1485         MUCServiceProperties properties = propertyMaps.get(subdomain);
1486         if (properties == null) {
1487             properties = new MUCServiceProperties(subdomain);
1488         }
1489         properties.localPut(name, value);
1490         propertyMaps.put(subdomain, properties);
1491     }
1492 
1493    /**
1494      * Sets multiple Jive properties at once. If a property doesn't already exists, a new
1495      * one will be created.
1496      *
1497     * @param subdomain the subdomain of the service to set properties for
1498      * @param propertyMap a map of properties, keyed on property name.
1499      */
setProperties(String subdomain, Map<String, String> propertyMap)1500     public static void setProperties(String subdomain, Map<String, String> propertyMap) {
1501         MUCServiceProperties properties = propertyMaps.get(subdomain);
1502         if (properties == null) {
1503             properties = new MUCServiceProperties(subdomain);
1504         }
1505         properties.putAll(propertyMap);
1506         propertyMaps.put(subdomain, properties);
1507     }
1508 
1509     /**
1510      * Deletes a Jive property. If the property doesn't exist, the method
1511      * does nothing. All children of the property will be deleted as well.
1512      *
1513      * @param subdomain the subdomain of the service to delete a property from
1514      * @param name the name of the property to delete.
1515      */
deleteProperty(String subdomain, String name)1516     public static void deleteProperty(String subdomain, String name) {
1517         MUCServiceProperties properties = propertyMaps.get(subdomain);
1518         if (properties == null) {
1519             properties = new MUCServiceProperties(subdomain);
1520         }
1521         properties.remove(name);
1522         propertyMaps.put(subdomain, properties);
1523     }
1524 
deleteLocalProperty(String subdomain, String name)1525     public static void deleteLocalProperty(String subdomain, String name) {
1526         MUCServiceProperties properties = propertyMaps.get(subdomain);
1527         if (properties == null) {
1528             properties = new MUCServiceProperties(subdomain);
1529         }
1530         properties.localRemove(name);
1531         propertyMaps.put(subdomain, properties);
1532     }
1533 
1534     /**
1535      * Resets (reloads) the properties for a specified subdomain.
1536      *
1537      * @param subdomain the subdomain of the service to reload properties for.
1538      */
refreshProperties(String subdomain)1539     public static void refreshProperties(String subdomain) {
1540         propertyMaps.replace(subdomain, new MUCServiceProperties(subdomain));
1541     }
1542 
1543 }
1544